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:
- A motor won’t start (EngineError),
- 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 }, |