Error Handling
undefined, null, void
undefined
-
var foo: u8 = undefined. -
Should not be thought of as no value, but as a way of telling the compiler that you are not assigning a value yet .
-
Any type may be set to undefined, but attempting to read or use that value is always considered a mistake.
null
-
var foo: ?u8 = null;. -
The "null" primitive value is a value that means "no value".
-
This is typically used with optional types as in the example above.
-
When
fooequalsnull, that's not a value of typeu8. It means there is no value of typeu8infooat all.
void
-
var foo: void = {};. -
"void" is a type , not a value.
-
It is the most common of the Zero Bit Types (those types which take up absolutely no space and have only a semantic value).
-
When compiled to executable code, zero bit types generate no code at all.
-
The above example shows a variable
fooof typevoidwhich is assigned the value of an empty expression. -
It's much more common to see
voidas the return type of a function that returns nothing.
Error Sets, Try, Catch
Error Sets
-
An error set is like an enum, where each error in the set is a value.
-
There are no exceptions in Zig; errors are values.
const FileOpenError = error{
AccessDenied,
OutOfMemory,
FileNotFound,
};
Union: Error
-
An error set type and another type can be combined with the
!operator to form an error union type. -
Values of these types may be an error value or a value of the other type .
-
If you call a function that returns an error union (
!T) without usingtryorcatch, the Zig compiler will emit a compile error, since there is no defined way to handle the possible error. -
anyerror-
Is the global error set, which due to being the superset of all error sets, can have an error from any set coerced to it. Its usage should generally be avoided.
-
-
In variables :
const maybe_error: AllocationError!u16 = 10; // `maybe_error` can be a `u16` or an error of type `AllocationError`. // `AllocationError!u16` means the type can be `AllocationError` or `u16`. const no_error = maybe_error catch 0; // If `maybe_error` contained an error, `no_error` would receive `0`. // Since `maybe_error` does not contain an error, `no_error` equals 10. -
In functions :
-
With AnyError :
fn mightFail(x: bool) !i32 { // `!i32` means the type can be **any error** or `i32`. if (x) { return error.SomeError; } return 42; } -
With ErrorSet :
const MyErrors = error{ OutOfMemory, InvalidInput }; fn example(x: bool) MyErrors!i32 { if (x) { return error.OutOfMemory; // valid. //return error.InvalidInput; // valid. //return error.SomeOtherError; // invalid. } return 42; }
-
-
Merge :
const A = error{ NotDir, PathNotFound }; const B = error{ OutOfMemory, PathNotFound }; const C = A || B;
Try
-
Used to propagate errors automatically .
-
If the operation results in an error, the error will be returned to the caller.
-
-
Note :
-
Zig's
tryandcatchare unrelated to try-catch in other languages. -
Zig does not let us ignore error unions via
_ = x;. We must unwrap it withtry,catch, orifby some means.-
_ = try x;or_ = x catch {};is possible.
-
-
-
Syntax sugar for
|err|:-
try xis shorthand forx catch |err| return err.
-
Catch
-
catchis used to handle errors directly , providing an alternative value or specific handling.-
In other words, a 'fallback value'.
-
"Could instead be a
noreturn- the type ofreturn,while (true)and others."
-
-
Basic :
const result = mightFail(false) catch -1; // If there is an error, result is -1. -
With Payload Capturing :
fn failingFunction() error{Oops}!void { return error.Oops; } fn main() !void { failingFunction() catch |err| { return; }; } -
With Payload Capturing and Blocks :
-
"If you want to provide a default value with
catchafter performing some logic, you can combinecatchwith named Blocks :" -
Source .
const a: ?std.json.Parsed(std.json.Value) = parseJson(allocator, "mapa/mapa.ldtkasd") catch |err| blk: { print("File not found {}\n", .{err}); break :blk null; }; -
-
With Payload Capturing and Switch :
fn mightFail(x: bool) !i32 { if (x) return 42; return error.SomeError; } pub fn main() void { const result = mightFail(false) catch |err| switch (err) { error.SomeError => -1, // Convert the error to -1 else => -999, // Capture other errors }; } -
My examples :
-
Potentially crashing the program if an error occurs:
const jsonParsed = try parseJson(allocator, mapa); defer jsonParsed.deinit(); -
Trying to continue program execution if an error occurs:
const jsonParsed: ?std.json.Parsed(std.json.Value) = parseJson(allocator, mapa) catch |err| a: { print("\nERROR | LDtkParser: {}, {s}\n", .{err, mapa}); break :a null; }; if (jsonParsed) |*jsonParsed_| { defer jsonParsed_.*.deinit(); //etc. }
-
Optionals
Union: Optionals (
?T
)
-
Used to store either
nullor a value of typeT. -
Unwrapping :
-
.?is a shorthand fororelse unreachable.-
This is used when you know it is impossible for an optional value to be null; using this to unwrap a
nullvalue is detectable illegal behaviour.
const expect = @import("std").testing.expect; test "orelse unreachable" { const a: ?f32 = 5; const b = a orelse unreachable; const c = a.?; try expect(b == c); try expect(@TypeOf(c) == f32); } -
-
orelse:-
Acts when the optional is
null. This unwraps the optional to its child type.
const expect = @import("std").testing.expect; test "orelse" { const a: ?f32 = null; const fallback_value: f32 = 0; const b = a orelse fallback_value; try expect(b == 0); try expect(@TypeOf(b) == f32); } -
-
Unwrapping in expressions and loops :
-
If :
const a: ?i32 = 5; // Method 1 if (a != null) { const value = a.?; _ = value; } // Method 2 var b: ?i32 = 5; if (b) |*value| { value.* += 1; } -
While :
var numbers_left: u32 = 4; fn eventuallyNullSequence() ?u32 { if (numbers_left == 0) return null; numbers_left -= 1; return numbers_left; } fn main() !void { var sum: u32 = 0; while (eventuallyNullSequence()) |value| { sum += value; } } -
As in the union example, the captured value is immutable, but we can still use a pointer capture to modify the value stored in
b.
-
-
Note :
-
Optional pointer and optional slice types do not take up any extra memory compared to non-optional ones.
-
This is because internally they use the 0 value of the pointer for
null.
-
-
Runtime Safety, Unreachable
Detectable illegal behaviour
-
Illegal behaviour will be caught (causing a panic) with safety on, but will result in undefined behaviour with safety off.
-
Users are strongly recommended to develop and test their software with safety on, despite its speed penalties.
-
Enabled:
test "out of bounds" { const a = [3]u8{ 1, 2, 3 }; var index: u8 = 5; const b = a[index]; _ = b; index = index; } -
Disabled:
test "out of bounds, no safety" { @setRuntimeSafety(false); const a = [3]u8{ 1, 2, 3 }; var index: u8 = 5; const b = a[index]; _ = b; index = index; }
Unreachable
-
unreachableis an assertion to the compiler that this statement will not be reached. -
It can tell the compiler that a branch is impossible, which the optimiser can then take advantage of.
-
Reaching an
unreachableis detectable illegal behaviour.
test "unreachable" {
const x: i32 = 1;
const y: u32 = if (x == 2) 5 else unreachable; // crashes if `unreachable` is reached.
_ = y;
}
const expect = @import("std").testing.expect;
fn asciiToUpper(x: u8) u8 {
return switch (x) {
'a'...'z' => x + 'A' - 'a',
'A'...'Z' => x,
else => unreachable, // crashes if `unreachable` is reached.
};
}
test "unreachable switch" {
try expect(asciiToUpper('a') == 'A');
try expect(asciiToUpper('A') == 'A');
}