All posts

Zig / Handling errors

Posted On 01.10.2022

A function in Zig can return either an error or an actual value, most of the time you will see something like this:

pub fn countTheSheep() !u32 {
  ...
}

The return type of these functions is the combination of an error set type and an expected return type, and the ! binary operator, also called an error union type:

<error set>!<expected type>

If the error set is omited, it will be inferred by the compiler.

It’s recommended to always use explicit error set instead of inferred.

An error set is the same as an enum, each error name will be assigned with an unsigned integer greater than 0. Each error name can have a doc comment too:

const EmotionalError = error {
  // Cuz I'm grumpy right now
  TheMusicIsTooLoud,
 
  TheSkyIsBlue,
 
  // I know, right?
  LifeIsTooHard
};

If you defined the same error name more than once (in two different sets — you can’t define the same error in the same set), it gets assigned the same integer value:

// This won't works
const EmotionalError = error {
  TheMusicIsTooLoud,
  TheSkyIsBlue,
  LifeIsTooHard,
  TheSkyIsBlue
};
 
// This works
const MidlifeCrisisError = error {
  // Not again...
  LifeIsTooHard
};

You can merge the error sets with || operator, when you do so, the doc comment from the left-hand set will override the doc comment from the right-hand set:

const LifeError = EmotionalError || MidlifeCrisisError;

The doc comment of LifeError.LifeIsTooHard is // I know, right?.

There are a couple of ways we can handle a function with an error return type:

  • Use catch: when you want to provide a default value in case of error, for example:

    fn getLen(input: [:0]const u8) !usize {
      return input.len;
    }
     
    var len = getLen("hello") catch 0;
    // or
    var len = getLen("hello") catch |err| {
      std.debug.print("Hey! There is an error, I'm gonna return 0");
      return 0;
    };
    

    If you are so sure that your code will works and the error will never happen, you can use catch unreachable. In this case, if you’re wrong, and the program is compiled in Debug or ReleaseSafe mode, the program will panic and crash. In ReleaseFast mode, it’s an undefined behavior.

  • Use try when you want to return the same error from he caller function, otherwise, continue:

    var len = try getLen("hello");
    std.debug.print("It works!", .{});
    

    In this example, if getLen returns an error, the caller function will be returned with the same error. Otherwise, the execution will be continued and "It works" will be printed.

    You can also use errdefer to evaluate an expression on the exit of current code block (just like defer), but if and only if the error is returned from this block.

    var len = try getLen("hello");
    errdefer print("Len is 5, it's bad!", .{});
    if (len == 5) return error.BadLen;
    std.debug.print("It works!", .{});
    
  • Use if with an error capture is also a good way in case you want to do more things depends on the result of the function:

    if (getLen("hello")) |len| {
      std.debug.print("Len = {}", .{len});
    } else |err| { // <-- capture err here
      std.debug.print("Oops! {}", .{err});
    }
    

Lastly, don’t forget to check out Zig’s documentation about Errors for more in-depth discussions about error handling!