# Propagating Errors

### Propagating Errors

Propagating an error means passing it to the caller instead of handling it immediately.

In Zig, this is normal. Many functions are not the right place to decide what an error means. A low-level function may know that opening a file failed, but it may not know whether the program should retry, create a default file, print a warning, or stop.

So the low-level function returns the error upward.

### The Basic Pattern

The most common propagation tool is `try`.

```zig
fn loadConfig() !void {
    try readConfigFile();
    try parseConfig();
    try validateConfig();
}
```

Read this as:

```text
read the config file;
if that fails, return the error;

parse the config;
if that fails, return the error;

validate the config;
if that fails, return the error;
```

If all three steps succeed, `loadConfig` succeeds.

If any step fails, `loadConfig` stops and returns that error to its caller.

### Why Propagation Is Useful

Imagine this call chain:

```text
main
  runApp
    loadConfig
      readConfigFile
```

If `readConfigFile` fails because the file is missing, should it print a message and exit?

Usually, no.

The file-reading function should only report what happened. It should not decide the whole program’s policy.

```zig
fn readConfigFile() ![]u8 {
    return try std.fs.cwd().readFileAlloc(
        allocator,
        "config.json",
        1024 * 1024,
    );
}
```

The higher-level function can decide what to do.

```zig
fn runApp() !void {
    const config_text = readConfigFile() catch |err| switch (err) {
        error.FileNotFound => {
            try createDefaultConfig();
            return;
        },
        else => return err,
    };

    try parseAndUseConfig(config_text);
}
```

Here, `runApp` handles one specific error and propagates the rest.

### Propagation Keeps Layers Clean

A good Zig program often has layers.

Low-level code does concrete work.

High-level code makes policy decisions.

For example:

```text
file module:
  open, read, write

parser module:
  tokenize, parse, validate

app module:
  decide what to do when something fails
```

The file module should not know the application’s user interface.

The parser module should not know whether errors should be printed, logged, or shown in a GUI.

So these modules usually propagate errors.

```zig
fn parseConfig(text: []const u8) !Config {
    const tokens = try tokenize(text);
    const tree = try parseTokens(tokens);
    return try buildConfig(tree);
}
```

Each step can fail. `parseConfig` does not handle every failure itself. It returns the failure to its caller.

### Propagating with `try`

This line:

```zig
const value = try operation();
```

is shorthand for:

```zig
const value = operation() catch |err| return err;
```

So `try` means:

```text
if this failed, return the same error from my function
```

The error travels upward one function at a time.

```zig
fn inner() !void {
    return error.Failed;
}

fn middle() !void {
    try inner();
}

fn outer() !void {
    try middle();
}
```

If `inner` returns `error.Failed`, then `middle` returns `error.Failed`, then `outer` returns `error.Failed`.

No exception is thrown. No hidden stack unwinding happens. Each function returns an error value normally.

### Propagation Requires Compatible Error Sets

If you propagate an error, your function’s error set must allow that error.

This works:

```zig
const ReadError = error{
    FileNotFound,
    PermissionDenied,
};

const LoadError = error{
    FileNotFound,
    PermissionDenied,
    InvalidFormat,
};

fn readFile() ReadError![]const u8 {
    return error.FileNotFound;
}

fn loadConfig() LoadError!Config {
    const text = try readFile();
    return try parseConfig(text);
}
```

`readFile` may return `FileNotFound` or `PermissionDenied`.

`loadConfig` may also return those errors, so propagation is valid.

If the outer error set does not include the inner error, Zig rejects the code.

```zig
const ReadError = error{
    PermissionDenied,
};

const LoadError = error{
    InvalidFormat,
};

fn readFile() ReadError![]const u8 {
    return error.PermissionDenied;
}

fn loadConfig() LoadError!Config {
    const text = try readFile(); // not allowed
    return try parseConfig(text);
}
```

`loadConfig` cannot promise only `InvalidFormat` while returning `PermissionDenied`.

The type must tell the truth.

### Widening the Error Set

One way to fix incompatible propagation is to widen the outer error set.

```zig
const LoadError = error{
    PermissionDenied,
    InvalidFormat,
};
```

Now `loadConfig` can propagate `PermissionDenied`.

This is the simplest fix when the lower-level error should be visible to callers.

### Mapping Errors

Sometimes you do not want to expose lower-level errors.

In that case, use `catch` to map one error into another.

```zig
const LoadError = error{
    CannotReadConfig,
    InvalidFormat,
};

fn loadConfig() LoadError!Config {
    const text = readFile() catch |err| switch (err) {
        error.FileNotFound => return error.CannotReadConfig,
        error.PermissionDenied => return error.CannotReadConfig,
    };

    return parseConfig(text) catch |err| switch (err) {
        error.BadSyntax => return error.InvalidFormat,
        error.MissingField => return error.InvalidFormat,
    };
}
```

Now callers of `loadConfig` see a cleaner API:

```text
CannotReadConfig
InvalidFormat
```

They do not need to know every detail from the file and parser layers.

This is not always better. It depends on the API. Sometimes callers need precise errors. Sometimes they need a simple category.

### Partial Handling

A common pattern is to handle one error and propagate the rest.

```zig
const file = std.fs.cwd().openFile("config.json", .{}) catch |err| switch (err) {
    error.FileNotFound => {
        try createDefaultConfig();
        return;
    },
    else => return err,
};
```

This code handles `FileNotFound`.

For all other errors, it propagates the original error with:

```zig
else => return err
```

This is useful when one error has a clear local answer, but the rest should still go upward.

### Propagation and Cleanup

When an error is propagated with `try`, cleanup still matters.

```zig
fn processFile() !void {
    const file = try std.fs.cwd().openFile("data.txt", .{});
    defer file.close();

    try readHeader(file);
    try readBody(file);
    try validateFile(file);
}
```

If `readBody(file)` fails, the function returns early.

Before it returns, this cleanup runs:

```zig
defer file.close();
```

This is why `try` and `defer` are often used together.

For resources that should be cleaned only on failure, use `errdefer`.

```zig
fn createBuffer(allocator: std.mem.Allocator) ![]u8 {
    const buffer = try allocator.alloc(u8, 4096);
    errdefer allocator.free(buffer);

    try initializeBuffer(buffer);

    return buffer;
}
```

If `initializeBuffer` fails, `buffer` is freed.

If the function succeeds, the buffer is returned to the caller.

### Do Not Propagate Everything Blindly

Propagation is useful, but not every error should travel forever.

At some point, the program needs a policy.

For example:

```zig
pub fn main() !void {
    try runApp();
}
```

This is acceptable for small programs. If `runApp` fails, the error reaches `main`.

For a real command-line program, you may want better user-facing behavior:

```zig
pub fn main() void {
    runApp() catch |err| {
        std.debug.print("error: {}\n", .{err});
        std.process.exit(1);
    };
}
```

Here, `main` is the policy boundary. It turns an internal error into a message and an exit code.

### Where Errors Should Stop

A useful rule is:

```text
propagate errors through mechanical layers;
handle errors at decision layers
```

Mechanical layers do work:

```text
read file
parse text
allocate memory
send request
decode response
```

Decision layers know context:

```text
show message to user
retry operation
use default config
abort startup
skip one bad record
```

Good error handling usually means errors travel upward until they reach code that has enough context to make a useful decision.

### The Core Idea

Propagating an error means returning it to your caller.

In Zig, you usually do this with `try`.

```zig
const value = try operation();
```

This means:

```text
use the value if operation succeeds;
return the error if operation fails
```

Propagation keeps low-level code simple and honest. Higher-level code can decide what the failure means.

