# Designing Error APIs

### Designing Error APIs

An error API is the part of your function signature that tells callers how failure works.

In Zig, this is not hidden. A function that can fail shows that fact in its return type:

```zig
fn loadConfig() !Config {
    // ...
}
```

The `!Config` return type says:

```text
this function returns either an error or a Config
```

That is already an API decision. You are telling callers that loading a config is not guaranteed to succeed.

But good error API design goes further. It asks:

What errors should callers see?

Which errors are implementation details?

Should errors be precise or broad?

Should this function return an error, an optional, or a normal value?

These choices affect how easy your code is to use.

### Start with the Caller

Do not design errors only from the inside of the function.

Design them from the caller’s point of view.

Suppose you write:

```zig
fn loadConfig() !Config {
    // ...
}
```

A caller wants to know:

```text
Can the file be missing?
Can the syntax be invalid?
Can memory allocation fail?
Can permission be denied?
Can the config version be unsupported?
```

If the caller needs to react differently to those cases, your API should expose them clearly.

For example:

```zig
const ConfigError = error{
    FileNotFound,
    PermissionDenied,
    InvalidSyntax,
    UnsupportedVersion,
    OutOfMemory,
};

fn loadConfig(allocator: std.mem.Allocator) ConfigError!Config {
    // ...
}
```

Now the caller has useful information.

### Prefer Meaningful Error Names

Good error names describe what went wrong.

Weak names:

```zig
error.Failed
error.Bad
error.Unknown
error.Error
```

These names do not help the caller decide what to do.

Better names:

```zig
error.FileNotFound
error.PermissionDenied
error.InvalidSyntax
error.MissingRequiredField
error.UnsupportedVersion
```

These names are concrete. They point to a real condition.

A good error name should make this kind of code readable:

```zig
const config = loadConfig(allocator) catch |err| switch (err) {
    error.FileNotFound => try createDefaultConfig(),
    error.InvalidSyntax => return error.ConfigIsInvalid,
    error.PermissionDenied => return error.CannotReadConfig,
    else => return err,
};
```

The error names explain the policy.

### Use Explicit Error Sets for Public APIs

For small private helpers, inferred error sets are often fine:

```zig
fn helper() !void {
    try stepOne();
    try stepTwo();
}
```

But for public APIs, an explicit error set is usually better:

```zig
const ParseError = error{
    EmptyInput,
    InvalidCharacter,
    UnterminatedString,
};

pub fn parse(text: []const u8) ParseError!Ast {
    // ...
}
```

This makes the function contract clear.

The caller can see the exact failure cases without reading the implementation.

### Keep Internal Errors Internal

A function may call lower-level code that returns detailed errors.

That does not mean all those errors should leak into your public API.

For example:

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

This function may return file system errors and allocation errors.

But a higher-level API might want a simpler contract:

```zig
const LoadConfigError = error{
    CannotReadConfig,
    InvalidConfig,
    OutOfMemory,
};

fn loadConfig(allocator: std.mem.Allocator) LoadConfigError!Config {
    const text = readConfigText(allocator) catch |err| switch (err) {
        error.OutOfMemory => return error.OutOfMemory,
        else => return error.CannotReadConfig,
    };
    defer allocator.free(text);

    return parseConfig(text) catch |err| switch (err) {
        error.OutOfMemory => return error.OutOfMemory,
        else => return error.InvalidConfig,
    };
}
```

Here, the lower-level details are mapped into a smaller public error set.

Callers of `loadConfig` do not need to know every file system error. They only need to know that the config could not be read.

### Do Not Hide Important Errors

Simplifying errors can be good, but do not erase information callers need.

This might be too vague:

```zig
const LoadError = error{
    Failed,
};
```

Now every failure becomes `error.Failed`.

The caller cannot tell whether the file is missing, unreadable, invalid, or unsupported.

A better design gives useful categories:

```zig
const LoadError = error{
    FileNotFound,
    CannotReadFile,
    InvalidSyntax,
    UnsupportedVersion,
    OutOfMemory,
};
```

This is still not every low-level detail, but it gives callers real choices.

### Use Optional for Normal Absence

Do not use errors when there is no failure.

For example, searching a list may simply find nothing.

```zig
fn findUser(users: []const User, id: u64) ?User {
    for (users) |user| {
        if (user.id == id) return user;
    }

    return null;
}
```

This is better than:

```zig
fn findUser(users: []const User, id: u64) !User {
    return error.NotFound;
}
```

Use an optional when absence is a normal result.

Use an error when something failed.

### Combine Error Union and Optional When Needed

Sometimes both ideas are needed.

```zig
fn findUserInDatabase(id: u64) !?User {
    // ...
}
```

This means:

```text
error: the database query failed
null: the query succeeded, but no user was found
User: the query succeeded, and the user was found
```

This is more precise than treating all missing users as errors.

A caller can handle the cases separately:

```zig
const maybe_user = try findUserInDatabase(42);

if (maybe_user) |user| {
    try showUser(user);
} else {
    try showNotFoundMessage();
}
```

First, `try` handles real failure.

Then, `if` handles ordinary absence.

### Include `OutOfMemory` Honestly

If a function allocates memory, allocation can fail.

That usually means `error.OutOfMemory` belongs in the error set.

```zig
const BuildError = error{
    OutOfMemory,
    InvalidInput,
};

fn buildMessage(allocator: std.mem.Allocator, text: []const u8) BuildError![]u8 {
    if (text.len == 0) return error.InvalidInput;
    return try allocator.dupe(u8, text);
}
```

Do not pretend allocation cannot fail unless the allocator or context truly guarantees it.

Zig makes allocation explicit, so error APIs should reflect allocation failure honestly.

### Decide Where Policy Belongs

Low-level code should usually report errors.

High-level code should usually decide policy.

Low-level function:

```zig
fn parsePort(text: []const u8) !u16 {
    return try std.fmt.parseInt(u16, text, 10);
}
```

High-level policy:

```zig
const port = parsePort(text) catch 8080;
```

The parser should not decide that `8080` is the default. The application should decide that.

This keeps reusable code clean.

### Avoid `anyerror` in Stable Interfaces

`anyerror` means any possible error.

```zig
fn loadConfig() anyerror!Config {
    // ...
}
```

This is flexible, but broad.

It tells the caller:

```text
anything might happen
```

That weakens the function contract.

For private code or quick programs, `anyerror` may be acceptable. For stable library APIs, prefer a named error set.

```zig
const LoadConfigError = error{
    FileNotFound,
    PermissionDenied,
    InvalidSyntax,
    OutOfMemory,
};

fn loadConfig(allocator: std.mem.Allocator) LoadConfigError!Config {
    // ...
}
```

This is easier for callers to handle correctly.

### Keep Error Sets Small Enough to Understand

An error set should be useful, not enormous.

Too small:

```zig
const Error = error{
    Failed,
};
```

Too broad:

```zig
const Error = error{
    FileNotFound,
    PermissionDenied,
    PathTooLong,
    NameTooLong,
    DeviceBusy,
    DiskQuota,
    NoSpaceLeft,
    InvalidSyntax,
    InvalidEscape,
    InvalidNumber,
    MissingField,
    DuplicateField,
    UnsupportedVersion,
    UnknownSection,
    OutOfMemory,
    Timeout,
    Interrupted,
    WouldBlock,
};
```

The right size depends on the caller.

If callers can act differently on each case, keep them separate.

If callers will always handle several cases the same way, grouping them may be better.

### Document Ownership with Errors

When a function returns allocated memory, its error behavior and ownership behavior should be clear.

```zig
fn readFileAlloc(
    allocator: std.mem.Allocator,
    path: []const u8,
) ![]u8 {
    // caller owns returned memory on success
}
```

The rule should be:

```text
on success, caller owns the returned value
on error, the function cleans up its partial work
```

Inside the function, `errdefer` helps enforce this:

```zig
fn makePair(allocator: std.mem.Allocator) !Pair {
    const left = try allocator.alloc(u8, 100);
    errdefer allocator.free(left);

    const right = try allocator.alloc(u8, 100);
    errdefer allocator.free(right);

    return Pair{
        .left = left,
        .right = right,
    };
}
```

If the second allocation fails, the first allocation is freed.

If the function succeeds, ownership moves to the caller.

### A Practical Checklist

When designing a Zig error API, ask these questions:

| Question | Good Design Pressure |
|---|---|
| Can this operation fail? | Use an error union. |
| Is absence normal? | Use an optional. |
| Can both failure and absence happen? | Use `!?T`. |
| Can the caller act differently on different failures? | Use precise error names. |
| Are lower-level errors too detailed? | Map them to a cleaner error set. |
| Does the function allocate? | Include `OutOfMemory` when appropriate. |
| Is this public API? | Prefer an explicit named error set. |
| Does the function return ownership on success? | Use `errdefer` for partial cleanup. |

### The Core Idea

Designing error APIs means designing how failure appears to callers.

A good Zig error API is honest, precise, and not more complicated than necessary.

It should tell the caller:

```text
this operation can fail;
these are the failures you may need to handle;
on success, this is what you receive;
on error, partial work has been cleaned up
```

That makes error handling part of the program’s structure, not an afterthought.

