Errors

for Zig 0.15.2 Buy

Errors

In Zig, runtime errors are handled by defining specific values like this:


const ErrorTypeName = error {

     ErrorName,

      …

     ErrorNameN,

}; 


What we've really done is name a set of errors. In Zig, an error is a value. It can be defined and handled in much the same way as enums.

When we want to indicate that an error occurred during execution  for some specific reason, we return a specific error type.

This error type may be defined by the language itself, come from an external library, or have been declared by us.

Let’s consider the case of errors in a drone engine:

  // possible errors when trying to start an engine

  const EngineError = error{

    Damaged,

    Autorepairing,

    NotConnected,

  };

With EngineError, we define the failures the drone engine we want to mount on the wing might experience. We can always modify EngineError, adding or removing errors, as long as we haven’t used them in the code.

Now, if an operation can’t be performed, for example, the engine is too damaged to start, we return the EngineError.Damaged error. This way, we don’t lose control over potential errors: we express and handle what can go wrong rather than hide it. Errors, just like enums and structs, help us shape the model of entities and relationships that appear in our code.

        If we consider: what failures could the “wing” have? We don’t know all the answers right now, but we can declare the error that seems obvious for now: that it doesn’t have enough power to carry the weight:

  // errors returned by the Wing

  const WingError = error{

    NotEnoughEngines,

  };

For now, we’ll include that error, indicating there aren’t enough (working) engines. If we identify other possible failures later, we’ll add them. Writing software is an iterative process: we adjust and expand as the system grows.

Error handling

If we want to see what happened, it’s useful to capture errors using Zig’s |payload|. So far, we’ve mostly used try to handle failures.

try

We don’t capture the error, we just call the function with try and let it propagate “upward”:

  // main !void { … the main function needs to be declared as potentially

  // returning an error (!) + another value

  // the function returns either an error or void

  try returns_void_or_error(x_param);

  const x_val = try returns_value_or_error(x_param);

In this case, if an error occurs, the parent function where the call is made automatically returns that same error.

Errors with if / else

  // the function returns an error if something fails and

  // void if everything goes well

  if (returns_void_or_error(x_param)) {

    // if there’s no error

  } else |err_x| {

    // capture the error so we can use it

  }

  // the function returns an error if something fails and

  // some other value if no errors occur

  if (returns_value_or_error(x_param)) |x_val| {

    // if no error, we capture x_val

  } else |err_x| {

    // we capture the error and can use it

  }

Errors with catch

  // the function returns an error or void

  _ = returns_value_or_error(x_param) catch |err_x| {

    // runs if there’s an error

  }

  // if the function returns something other than void

  // we can capture it in a variable instead of _

  // we reach this point if there’s no error

The wing with engine errors

We don’t have much time; we need to assemble the “wing” with the materials we’ve got before they find out we’ve escaped. They have the law on their side, tailored to fit them, but in truth, they lack any deep understanding of how things actually work. They think they’ve defeated us, and that gives us a certain advantage.

wing.zig

const std = @import("std");

const print = std.debug.print;

// possible engine errors

const EngineError = error{

    Damaged,

    Autorepairing,

    NotConnected,

};

// errors returned by the Wing

const WingError = error{

    NotEnoughEngines,

};

// union of error types that

// make flight impossible

const CriticalFailure = EngineError || WingError;

// possible engine states

const EngineStatus = enum(i8) {

    failure = -1,

    off = 0,

    on = 1,

    testing = 2,

    autorepairing = 3,

};

// engine as a struct

const DroneEngine = struct {

    var n_total_failed: u8 = 0;

    const N_MAX_RPM: u32 = 8500;

    const N_MIN_HEALTH: u32 = 75;

    e_status: EngineStatus,

    n_health: u8,

    s_key: []const u8,

    n_rpm: u32,

    // the engine on/off button

    fn toggle(self: *DroneEngine) EngineError!void {

        self.e_status = sw: switch (self.e_status) {

            EngineStatus.off => try self.turn_on(),

            // if it’s on, we turn it off

            EngineStatus.on => {

                self.turn_off();

                break :sw self.e_status;

            },

            EngineStatus.testing => EngineStatus.off,

            EngineStatus.autorepairing => EngineStatus.off,

            EngineStatus.failure => EngineStatus.failure,

        };

    }

    // turning off simply cuts the power supply

    fn turn_off(self: *DroneEngine) void {

        self.e_status = .off;

        self.n_rpm = 0;

    }

    // function that checks whether the conditions

    // to be able to start the engine are met

    fn turn_on(self: *DroneEngine) EngineError!EngineStatus {

        if (self.n_health > DroneEngine.N_MIN_HEALTH) {

            // the engine turns on

            self.e_status = EngineStatus.on;

            self.n_rpm = DroneEngine.N_MAX_RPM * self.n_health / 100;

        } else if (self.n_health > 20) {

            // the engine attempts self-repair

            self.e_status = EngineStatus.autorepairing;

            self.n_rpm = DroneEngine.N_MAX_RPM * self.n_health / 100;

            DroneEngine.n_total_failed += 1;

            return EngineError.Autorepairing;

        } else {

            // failure

            self.e_status = EngineStatus.failure;

            self.n_rpm = 0;

            DroneEngine.n_total_failed += 1;

            return EngineError.Damaged;

        }

        return self.e_status;

    }

    fn set_rpm(self: *DroneEngine, n_rpm: u32) bool {

        if (self.e_status != EngineStatus.on and

            self.e_status != EngineStatus.testing) return false;

        self.n_rpm = if (n_rpm > DroneEngine.N_MAX_RPM)

            DroneEngine.N_MAX_RPM

        else if (n_rpm < 0) 0 else n_rpm;

        return true;

    }

    // method that formats the struct and is invoked

    // automatically when we print it

    pub fn format(self: DroneEngine, writer: anytype) !void {

        try writer.print(

            \\Engine {s}:

            \\  * rpm: {}

            \\  * health: {}

            \\  * status: {s}

        , .{

            self.s_key,

            self.n_rpm,

            self.n_health,

            @tagName(self.e_status),

        });

    }

};

// all available engines

const a_all_engines = [_]DroneEngine{

    DroneEngine{ .s_key = "E1", .n_health = 100, .n_rpm = 0, .e_status = .off },

    DroneEngine{ .s_key = "E2", .n_health = 95, .n_rpm = 0, .e_status = .off },

    DroneEngine{ .s_key = "E3", .n_health = 100, .n_rpm = 0, .e_status = .off },

    DroneEngine{ .s_key = "E6", .n_health = 0, .n_rpm = 0, .e_status = .off },

    DroneEngine{ .s_key = "E4", .n_health = 98, .n_rpm = 0, .e_status = .off },

    DroneEngine{ .s_key = "E5", .n_health = 60, .n_rpm = 0, .e_status = .off },

};

// slots to mount the engines

const a_placeholders = .{DroneEngine{

    .s_key = "EMPTY",

    .n_health = 0,

    .n_rpm = 0,

    .e_status = .failure,

}} ** 4;

// Wing as a struct

const Wing = struct {

    // all engines will be mounted in their slots

    a_engines: [4]DroneEngine = a_placeholders,

    // mount the engines on the wing

    fn mount_engines(self: *Wing, a_engines2mount: *const [4]DroneEngine) void {

        for (&self.a_engines, 0..) |_, n_i| {

            self.a_engines[n_i] = a_engines2mount[n_i];

        }

    }

    // turning on the wing may result in a critical error

    // or return nothing -> !void

    fn turn_on(self: *Wing) CriticalFailure!void {

        // reset errors when turning on the wing

        DroneEngine.n_total_failed = 0;

        // if there is any critical failure

        // we turn off all previously started engines

        errdefer {

            for (&self.a_engines, 0..) |*o_engine, n_i| {

                if (o_engine.e_status == .on) {

                    o_engine.turn_off();

                    print("Turning off engine: {}\n", // reports shutdown

                        .{n_i + 1});

                }

            }

        }

        for (&self.a_engines, 0..) |*o_engine, n_i| {

            if (o_engine.e_status == .off) {

                o_engine.toggle() catch |errx| {

                    print("At Engine Socket: {} ERR: {}\n", // reports

                        .{ n_i + 1, errx });

                };

            }

        }

        // if any engine fails, we return the error

        // the wing needs all 4 engines to be able to fly

        // with the weight of the player and 1KR

        if (DroneEngine.n_total_failed > 0) 

          return WingError.NotEnoughEngines;

    }

    // print the state of the engines

    fn show_state(self: *Wing) void {

        print("-------------------------------------------------\n", .{});

        for (&self.a_engines) |*o_engine| {

            print("{f}\n", .{o_engine});

        }

        print("\n{} failures\n", .{DroneEngine.n_total_failed});

        print("-------------------------------------------------\n", .{});

    }

    // engine speed control

    fn set_rpm_by_id(self: *Wing, n_engine: usize, n_rpm: u32) void {

        const n_engine_ix = if (n_engine < 1)

            0

        else if (n_engine > self.a_engines.len)

            self.a_engines.len - 1

        else

            n_engine - 1;

        _ = &self.a_engines[n_engine_ix].set_rpm(n_rpm);

    }

};

// we have function calls that return errors

// but we handle them with if; if we used try

// we would need to use !void

pub fn main() void {

    // the empty wing

    var o_wing = Wing{};

    // show the empty wing

    o_wing.show_state();

    // mount 4 of the available engines

    o_wing.mount_engines(a_all_engines[0..4]);

    // show the state of the wing

    o_wing.show_state();

    // turn on the wing

    if (o_wing.turn_on()) |_| {

        // if there are no errors

        // we try to adjust the speed of an engine

        o_wing.set_rpm_by_id(2, 5000);

        // if there is an error, we show it

    } else |err_wing| print("Wing ERROR: {}\n", .{err_wing});

    o_wing.show_state();

}

$ zig run wing.zig

...

At Engine Socket: 4 ERR: error.Damaged

Turning off engine: 1

Turning off engine: 2

Turning off engine: 3

Wing ERROR: error.NotEnoughEngines

-------------------------------------------------

Engine E1:

  * rpm: 0

  * health: 100

  * status: off

Engine E2:

  * rpm: 0

  * health: 95

  * status: off

Engine E3:

  * rpm: 0

  * health: 100

  * status: off

Engine E6:

  * rpm: 0

  * health: 0

  * status: failure

1 failures

-------------------------------------------------

As we know, on the beach, there are six engines removed from the ARP1 drones:

  // all available engines

  const a_all_engines = [_]DroneEngine{

    DroneEngine{ .s_key = "E1", .n_health = 100, .n_rpm = 0,

    ...

The first four have been mounted on the “wing” ([0..4]), and the player tried to start it:

    // mount 4 of the available engines

    o_wing.mount_engines(a_all_engines[0..4]);

    // we turn on the wing

    if (o_wing.turn_on()) |_| {

But an engine error and a “wing” error were triggered:

...

-------------------------------------------------

At Engine Socket: 4 ERR: error.Damaged

Turning off engine: 1

Turning off engine: 2

Turning off engine: 3

Wing ERROR: error.NotEnoughEngines

-------------------------------------------------

...

Engine E6:

  * rpm: 0

  * health: 0

  * status: failure

1 failures

-------------------------------------------------

Merging sets of errors

The “wing” may fail to take off for two reasons:

  1. A motor won’t start (EngineError),
  2. There aren’t enough working engines mounted (WingError),

Both cases are treated as critical failures, which we’ve combined with ||.

  // union of error types that

  // make flight impossible

  const CriticalFailure = EngineError || WingError;

This way, our turn_on function returns any possible error, whether from the engine or the wing. What matters to us is whether the flight is possible.

errdefer

Luckily, the system shut down the engines that had been started previously as soon as an error was detected. This is done using errdefer, a reserved keyword, cousin of defer. While defer postpones execution of a command until the current block closes, errdefer does the same but only if the function returns an error:

  fn turn_on(self: *Wing) CriticalFailure!void {

    //...

    errdefer {

       // code that runs only if turn_on returns an error

    }

    //... if ...

    return WingError.NotEnoughEngines;

  }

Both errdefer and defer execute their deferrals in LIFO order (last in, first out) - like a stack.

Possible fixes

Do you spot the error?

I'll let you think for a moment while looking at that startup log.

The player, exhausted as he was, got confused and mounted engine E6 in the fourth slot-and that engine, as we already know, is completely broken. It won’t work.

How would you fix it?

Here’s a hint:

  const a_all_engines = [_]DroneEngine{

    ...

    // with a simple comment, you could set this engine aside

    DroneEngine{ .s_key = "E6", .n_health = 0, .n_rpm = 0, .e_status = .off },

Structs
Unions
© 2025 - 2026 Zen of Zig