Zig by example
In Zig, errors are values. There are no exceptions in Zig.
Defining an error is done with the error keyword:
error.NotEven
A type can be prefixed with the !
operator to indicate that it is an error union. An error union is a type that can be either the type that is specified or any error.
In Zig, it is very common to see functions that return error unions. Here is an example function that returns either a u8
or an error:
fn isEven(number: u8) !u8 {
if (number % 2 == 0) {
return number;
} else {
return error.NotEven;
}
}
Let’s call this function twice. The first time, it will execute and return a u8
. The second time, it will return an error:
var result: anyerror!u8 = isEven(2);
print("Result isEven(2):\n{any}\n", .{result});
result = isEven(3);
print("Result isEven(3):\n{any}\n", .{result});
When we run this code, the following is printed to screen:
Result isEven(2):
2
Result isEven(3):
error.NotEven
A common pattern you will see in Zig is payload capturing. This is where you capture the value of an error in a variable using the catch
keyword. Here is an example of a function that uses the catch
keyword:
fn useCatch(number: u8) !u8 {
const result = isEven(number) catch |err| return err;
return result;
}
result = useCatch(2);
print("Result useCatch(2):\n{any}\n", .{result});
result = useCatch(3);
print("Result useCatch(3):\n{any}\n", .{result});
When isEven
is called in this function, and when it produces an error, the catch
statement will capture an error value inside the err
variable. If there is an error value, the function will return that error.
If isEven
executes without any error, the function will continue to run. The previous example returns the following:
Result useCatch(2):
2
Result useCatch(3):
error.NotEven
There is also a shorthand for the catch
keyword. Next, we use catch
to indicate what value we want to work with in case the function returns an error:
var u8_result: u8 = isEven(2) catch 0;
print("isEven(2) catch 0;\n{any}\n", .{u8_result});
u8_result = isEven(3) catch 0;
print("isEven(3) catch 0;\n{any}\n", .{u8_result});
Notice that we can now capture the result of the function in a value of type u8
. We can do this is because the compiler understands that the expression will always result in a u8
, even when isEven
returns an error. Basically, this allows you to specify a default value for this function while handling the possible error the function may return.
The previous code produces the following result:
isEven(2) catch 0;
2
isEven(3) catch 0;
0
Zig also offers try
. This is shorthand for catch |err| return err
. In the following example, we define a function in which we call isEven
. We use try
when we call the function. What this will do is return the error if the isEven
function returns an error value. If the isEven
function returns a non-error value, the rest of the function continues to execute.
Also notice in this case the return value of the function:
fn useTry(number: u8) !u8 {
const result = try isEven(number);
print("useTry body executes for number {d}\n", .{number});
return result;
}
The function return is of the type !u8
. This means the function returns an error union, which is either an error or a u8
.
The first line will try
to run the isEven
function. If the function does not produce an error, the rest of the function will execute normally:
result = useTry(2);
This will print the following to screen:
useTry body executes for number 2
The function ran all the way to the end and it returned a u8
.
result = useTry(3);
Now observe the following:
result = useTry(3);
print("Result useTry(3):\n{any}\n", .{result});
The isEven
function will now produce an error, causing the function to stop and return immediately. When we run it, we see the following:
Result useTry(3):
error.NotEven
We notice that the print statement from the function body was not executed. Instead, it immediately returned the error value error.NotEven
that was returned by the isEven
function.
The source code for the examples can be found here.