# Custom Error Types

### Custom Error Types

Most Zig programs start with small error sets:

```zig
const ParseError = error{
    InvalidDigit,
    EmptyInput,
};
```

This is enough for many cases.

But larger programs often need more structure. Different modules may define their own error sets. Some errors may need to be grouped, mapped, or exposed differently across API boundaries.

This chapter explains how to design and organize your own error types.

### Error Sets Are Types

An error set is a real Zig type.

```zig
const NetworkError = error{
    ConnectionFailed,
    Timeout,
    ConnectionClosed,
};
```

`NetworkError` is now a named type.

You can use it in function signatures:

```zig
fn connect() NetworkError!void {
    return error.Timeout;
}
```

You can store it in variables:

```zig
const err: NetworkError = error.Timeout;
```

You can compare it:

```zig
if (err == error.Timeout) {
    // ...
}
```

Error sets are lightweight typed collections of named failures.

### Separate Error Types by Domain

A good rule is:

```text
define errors near the subsystem that owns them
```

For example:

```zig
const FileError = error{
    FileNotFound,
    PermissionDenied,
    DiskFull,
};

const ParseError = error{
    InvalidSyntax,
    UnexpectedToken,
    UnterminatedString,
};

const NetworkError = error{
    Timeout,
    ConnectionClosed,
    InvalidPacket,
};
```

Each subsystem describes its own failures.

This makes the code easier to reason about.

A parser should not return `DiskFull`.

A networking module should not return `UnexpectedToken`.

The error types describe the domain.

### Grouping Related Errors

Suppose a config loader reads a file and parses it.

Internally, it may use two subsystems:

```text
file reading
config parsing
```

Each subsystem has its own error set.

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

const ParseError = error{
    InvalidSyntax,
    MissingField,
};
```

You can combine them:

```zig
const ConfigError = FileError || ParseError;
```

Now `ConfigError` contains:

```text
error.FileNotFound
error.PermissionDenied
error.InvalidSyntax
error.MissingField
```

This is useful when a higher-level operation depends on multiple lower-level systems.

### Example: Config Loader

```zig
const std = @import("std");

const FileError = error{
    FileNotFound,
    PermissionDenied,
};

const ParseError = error{
    InvalidSyntax,
    MissingField,
};

const ConfigError = FileError || ParseError;

fn readConfigFile() FileError![]const u8 {
    return error.FileNotFound;
}

fn parseConfig(text: []const u8) ParseError!void {
    _ = text;
    return error.InvalidSyntax;
}

fn loadConfig() ConfigError!void {
    const text = try readConfigFile();
    try parseConfig(text);
}
```

`loadConfig` can now propagate errors from both subsystems.

### Creating Application-Level Errors

Sometimes you do not want callers to see all internal errors.

Suppose your application only wants these public errors:

```zig
const AppError = error{
    ConfigFailed,
    NetworkFailed,
};
```

You can map lower-level errors into these broader categories.

```zig
fn startApplication() AppError!void {
    loadConfig() catch {
        return error.ConfigFailed;
    };

    connectToServer() catch {
        return error.NetworkFailed;
    };
}
```

This creates a cleaner public API.

The caller does not need to know whether config loading failed because of invalid syntax or missing permissions. The application treats all config problems the same way.

### Precise Errors vs Broad Errors

There is a tradeoff.

Very precise error sets:

```zig
const ParseError = error{
    InvalidCharacter,
    InvalidEscape,
    UnterminatedString,
    InvalidNumber,
    MissingComma,
    MissingColon,
    DuplicateKey,
};
```

Advantages:

```text
better debugging
more control for callers
more detailed diagnostics
```

Disadvantages:

```text
larger APIs
more handling logic
more maintenance
```

Very broad error sets:

```zig
const ParseError = error{
    InvalidInput,
};
```

Advantages:

```text
simple API
easy handling
```

Disadvantages:

```text
less information
harder debugging
less caller control
```

The correct size depends on the use case.

Library code often benefits from more precision.

Application-level code often benefits from simpler categories.

### Error Translation

One subsystem may expose detailed errors internally but simpler errors externally.

Example:

```zig
const JsonError = error{
    InvalidEscape,
    InvalidUnicode,
    UnexpectedEnd,
};

const ConfigError = error{
    InvalidConfig,
};
```

Translate like this:

```zig
fn loadConfig(text: []const u8) ConfigError!void {
    parseJson(text) catch {
        return error.InvalidConfig;
    };
}
```

This is called error translation or error mapping.

It creates a boundary between internal implementation details and external API design.

### Custom Error Types for Libraries

A library should usually define its own named error sets.

Good:

```zig
pub const HttpError = error{
    InvalidUrl,
    ConnectionFailed,
    Timeout,
    InvalidResponse,
};
```

Less good:

```zig
pub fn request() anyerror!Response {
    // ...
}
```

Why?

Because callers need to know what failures are part of the contract.

Named error sets become documentation.

A caller can inspect the type and understand the API.

### Shared Error Names

Error names are global.

These refer to the same error name:

```zig
error.Timeout
```

even if they appear in different error sets.

```zig
const NetworkError = error{
    Timeout,
};

const DatabaseError = error{
    Timeout,
};
```

Both contain the same global error name: `error.Timeout`.

This is why error names should be reasonably generic and reusable.

Good reusable names:

```zig
error.Timeout
error.OutOfMemory
error.PermissionDenied
error.ConnectionClosed
```

Overly specific names can become awkward:

```zig
error.HttpClientConnectionClosedUnexpectedlyDuringUpload
```

If an error name becomes extremely long, it may indicate the API boundary is wrong or the subsystem responsibilities are mixed together.

### Error Names Are Not Messages

An error name is not a user-facing string.

This is not ideal:

```zig
error.YouEnteredAnInvalidPasswordPleaseTryAgain
```

Use concise symbolic names:

```zig
error.InvalidPassword
```

Then convert them into user-facing messages separately.

```zig
login() catch |err| switch (err) {
    error.InvalidPassword => {
        std.debug.print("incorrect password\n", .{});
    },
    error.AccountLocked => {
        std.debug.print("account locked\n", .{});
    },
};
```

This separation matters because:

```text
internal error names are for programmers
messages are for users
```

### Extending Error Sets

You can build larger error sets from smaller ones.

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

const DecodeError = error{
    InvalidFormat,
};

const ImageError = ReadError || DecodeError || error{
    UnsupportedColorMode,
};
```

Now `ImageError` contains all these errors together.

This helps when constructing layered systems.

### Returning Different Error Sets

Different functions can expose different levels of detail.

Low-level function:

```zig
fn tokenize(text: []const u8) TokenizeError!Tokens {
    // ...
}
```

Higher-level parser:

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

Application-level API:

```zig
fn compile(text: []const u8) CompileError!Binary {
    // ...
}
```

Each layer decides how much detail to expose.

That is part of API design.

### Avoid Giant Global Error Sets

A common beginner mistake is creating one huge error type for the whole program.

```zig
const AppError = error{
    FileNotFound,
    PermissionDenied,
    InvalidSyntax,
    ConnectionClosed,
    Timeout,
    DiskFull,
    MissingField,
    InvalidPassword,
    DatabaseCorrupted,
    UnsupportedVersion,
    OutOfMemory,
    // hundreds more...
};
```

This becomes difficult to understand and maintain.

Prefer smaller subsystem-level error sets.

Combine them only when needed.

### Custom Errors and Testing

Precise error types also improve tests.

Example:

```zig
try std.testing.expectError(
    error.InvalidSyntax,
    parseConfig("{ invalid"),
);
```

This test checks for one exact failure.

Precise errors make tests stronger and easier to debug.

### A Realistic Layered Example

Imagine this structure:

```text
lexer
parser
compiler
application
```

Each layer has its own errors.

```zig
const LexerError = error{
    InvalidCharacter,
    UnterminatedString,
};

const ParserError = LexerError || error{
    UnexpectedToken,
};

const CompileError = ParserError || error{
    UnknownVariable,
    InvalidType,
};
```

Now higher layers naturally inherit lower-layer failures.

The types describe the architecture.

### The Core Idea

Custom error types let you organize failure intentionally.

A good error type:

```text
belongs to a subsystem
describes meaningful failures
helps callers make decisions
does not expose unnecessary details
```

Error sets are not only about reporting problems. They are part of the structure of the program itself.

