Errors
In ZIG, errors are juste values, those values are those of a special type of enum
, that looks just like an enum but with the error
keyword added.
const TcpConnectionError = error{
HandshakeFailed,
ConnectionClosed,
UnexpectedData,
};
When you declare your enum using this keyword instead of enum
, the compiler is going to know that those values are going to be errors, therefore they can be catched, tried and so on.
If you want to return an error without having to make a whole enum for that, you can just return error.MyPeronalError
like so.
return error.MyPersonalError;
Handling errors in the application flow
Functions can return either a value or an error, this is done by adding the !
to the return type of a function.
With the !
, the return type of the function below is the coerced anyerror!u8
, which means that it can either return the excepted value or an error.
This way of handling errors is different from languages like Rust for example where you encapsulate the error or the value in a completly different type, the Result
type.
Note that since error sets are just enums, under the hood all the errors are just u16
by default.
It is important the the syntax of the type is ErrorSet!Payload
and not Payload!ErrorSet
. You first have to precise the error and then the payload, it is not possible to mix them up.
When a function can return an error we have to either try
or catch
it.
try
is going to either get the result or make the function return the error. If your function contains a try
statement, it has to return a !T
type, where T
it the type of the excepted value.
catch
is going to treat the error and return a value even if an error occured.
fn maybeErrorMaybeValue(isError: bool) !u8 {
// ... might return an u8 or an error
if (isError) {
return error.Bar;
} else {
return 2;
}
}
test "return value or error" {
// "try" version
const res = try maybeErrorMaybeValue(false); // if error, return the error
try std.testing.expect(res == 2);
// "catch" version
const err = maybeErrorMaybeValue(true) catch 3;
try std.testing.expect(err == 3);
}
test "should panic" {
_ = maybeErrorMaybeValue(true) catch unreachable;
}
Sometime we want to treat the error depending on the type of the error, to do that we capture the error. Here the upgradeBuilding()
function is either return a value or an error. Here is an example taken from my project where we do different actions depending on the type of the error.
building.upgradeBuilding(ctx.app.db) catch |err| {
if (err == error.NotEnoughRessources) {
try res.json(.{ .message = "Not enough ressources" }, .{});
return;
}
try res.json(.{ .message = "Unexcepted error occured while upgrading the building" }, .{});
return;
};
anyerror
This is the global error set containing all the possible errors that can happen in the current compilation unit. It is the superset of all the errors.
This is the default error used when you just add the !
withtout precsing the error type. Which means that !u64
is just a syntaxic sugar for anyerror!u64
.
Errdefer
This exemple is taken as if from the official documentation. errdefer is a particularity of ZIG, it allows the user to execute some code when the function returns an error, it is useful for exemple for deallocating variables you would have normally returned from the function, but since the function failed you deallocate that memory to avoid a memory leak.
fn createFoo(param: i32) !Foo {
const foo = try tryToAllocateFoo();
// now we have allocated foo. we need to free it if the function fails.
// but we want to return it if the function succeeds.
errdefer deallocateFoo(foo);
const tmp_buf = allocateTmpBuffer() orelse return error.OutOfMemory;
// tmp_buf is truly a temporary resource, and we for sure want to clean it up
// before this block leaves scope
defer deallocateTmpBuffer(tmp_buf);
if (param > 1337) return error.InvalidParam;
// here the errdefer will not run since we're returning success from the function.
// but the defer will run!
return foo;
}