Skip to main content

Errors

An error set is like an enum (details on Zig's enums later), where each error in the set is a value. There are no exceptions in Zig; errors are values. Let's create an error set.

const FileOpenError = error{
AccessDenied,
OutOfMemory,
FileNotFound,
};

Error sets coerce to their supersets.

const expect = @import("std").testing.expect;

const FileOpenError = error{
AccessDenied,
OutOfMemory,
FileNotFound,
};
const AllocationError = error{OutOfMemory};

test "coerce error from a subset to a superset" {
const err: FileOpenError = AllocationError.OutOfMemory;
try expect(err == FileOpenError.OutOfMemory);
}

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.

Let's create a value of an error union type. Here catch is used, which is followed by an expression which is evaluated when the value preceding it is an error. The catch here is used to provide a fallback value, but could instead be a noreturn - the type of return, while (true) and others.

const expect = @import("std").testing.expect;

const FileOpenError = error{
AccessDenied,
OutOfMemory,
FileNotFound,
};
const AllocationError = error{OutOfMemory};

test "error union" {
const maybe_error: AllocationError!u16 = 10;
const no_error = maybe_error catch 0;

try expect(@TypeOf(no_error) == u16);
try expect(no_error == 10);
}

Functions often return error unions. Here's one using a catch, where the |err| syntax receives the value of the error. This is called payload capturing, and is used similarly in many places. We'll talk about it in more detail later in the chapter. Side note: some languages use similar syntax for lambdas - this is not true for Zig.

const expect = @import("std").testing.expect;

fn failingFunction() error{Oops}!void {
return error.Oops;
}

test "returning an error" {
failingFunction() catch |err| {
try expect(err == error.Oops);
return;
};
}

try x is a shortcut for x catch |err| return err, and is commonly used where handling an error isn't appropriate. Zig's try and catch are unrelated to try-catch in other languages.

const expect = @import("std").testing.expect;

fn failingFunction() error{Oops}!void {
return error.Oops;
}

fn failFn() error{Oops}!i32 {
try failingFunction();
return 12;
}

test "try" {
var v = failFn() catch |err| {
try expect(err == error.Oops);
return;
};
try expect(v == 12); // is never reached
}

errdefer works like defer, but only executing when the function is returned from with an error inside of the errdefer's block.

const expect = @import("std").testing.expect;

fn failingFunction() error{Oops}!void {
return error.Oops;
}

var problems: u32 = 98;

fn failFnCounter() error{Oops}!void {
errdefer problems += 1;
try failingFunction();
}

test "errdefer" {
failFnCounter() catch |err| {
try expect(err == error.Oops);
try expect(problems == 99);
return;
};
}

Error unions returned from a function can have their error sets inferred by not having an explicit error set. This inferred error set contains all possible errors that the function may return.

fn createFile() !void {
return error.AccessDenied;
}

test "inferred error set" {
//type coercion successfully takes place
const x: error{AccessDenied}!void = createFile();

//Zig does not let us ignore error unions via _ = x;
//we must unwrap it with "try", "catch", or "if" by any means
_ = x catch {};
}

Error sets can be merged.

const A = error{ NotDir, PathNotFound };
const B = error{ OutOfMemory, PathNotFound };
const C = A || B;

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 be generally avoided.