🎓 A1Learner
🔍
0 of 46 lessons completed

Zig Error Traces

An error trace is a record of the calls an error passed through before it was handled. This lesson shows how to read its depth without printing anything that changes between runs.

Before, during, after

Before any error the index is zero, inside the handler it is one, and after handling it is zero again.

const std = @import("std");
const TraceError = error{ Boom };
fn boom() TraceError!void {
    return TraceError.Boom;
}
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    if (@errorReturnTrace()) |t| {
        try out.print("before: {d}\n", .{t.index});
    }
    boom() catch {
        if (@errorReturnTrace()) |t| {
            try out.print("in catch: {d}\n", .{t.index});
        }
    };
    if (@errorReturnTrace()) |t| {
        try out.print("after: {d}\n", .{t.index});
    }
}

Depth grows with layers

Each added layer adds one to the index.

const std = @import("std");
const TraceError = error{ Boom };
fn one() TraceError!void {
    return TraceError.Boom;
}
fn two() TraceError!void {
    try one();
}
fn three() TraceError!void {
    try two();
}
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    one() catch {
        if (@errorReturnTrace()) |t| {
            try out.print("depth 1: {d}\n", .{t.index});
        }
    };
    two() catch {
        if (@errorReturnTrace()) |t| {
            try out.print("depth 2: {d}\n", .{t.index});
        }
    };
    three() catch {
        if (@errorReturnTrace()) |t| {
            try out.print("depth 3: {d}\n", .{t.index});
        }
    };
}

Deep versus shallow

A three-layer chain reports a depth of three, while a direct call reports one.

const std = @import("std");
const TraceError = error{ Boom };
fn read() TraceError!void {
    return TraceError.Boom;
}
fn parse() TraceError!void {
    try read();
}
fn load() TraceError!void {
    try parse();
}
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    load() catch |err| {
        try out.print("error: {s}\n", .{@errorName(err)});
        if (@errorReturnTrace()) |t| {
            try out.print("trace depth: {d}\n", .{t.index});
        }
    };
    read() catch {
        if (@errorReturnTrace()) |t| {
            try out.print("shallow depth: {d}\n", .{t.index});
        }
    };
}

Example

const std = @import("std");
const PipeError = error{ NotFound, BadData };
fn read(ok: bool) PipeError!i32 {
    if (ok) return 8;
    return PipeError.NotFound;
}
fn parse(ok: bool) PipeError!i32 {
    const v = try read(ok);
    if (v == 0) return PipeError.BadData;
    return v * 2;
}
fn run(ok: bool) PipeError!i32 {
    return try parse(ok);
}
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    if (run(true)) |v| {
        try out.print("result: {d}\n", .{v});
    } else |err| {
        try out.print("failed: {s}\n", .{@errorName(err)});
    }
    if (run(false)) |v| {
        try out.print("result: {d}\n", .{v});
    } else |err| {
        try out.print("failed: {s}\n", .{@errorName(err)});
        if (@errorReturnTrace()) |t| {
            try out.print("trace depth: {d}\n", .{t.index});
        }
    }
}

Key points

  • Error trace: an error trace is a record of the calls an error passed through on its way to a handler.
  • Error trace function: an error trace function is @errorReturnTrace, a built-in function that returns the current error trace.
  • Trace index: a trace index is the count of calls recorded in the trace, and it grows with the depth of the call chain.
  • Presence alone proves nothing, because the trace value exists even before any error, with an index of zero.
  • The index is most useful inside a handler, while the error is still travelling, since it resets after handling.
  • A trace holds return addresses, and addresses change from run to run, so print only the index, never an address.

run calls parse, which calls read, so a failure travels through three layers. The handler prints the error name and the trace depth together. That pair tells the reader what failed and how deep the failure started, without printing any address.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

A chain of three functions returns an error to main. What trace index does the handler in main see?