Programs fail for many reasons. A file may not exist. Memory allocation may fail. Input may be malformed. A network connection may close unexpectedly.
Programs fail for many reasons. A file may not exist. Memory allocation may fail. Input may be malformed. A network connection may close unexpectedly.
Zig treats errors as values.
An error in Zig is not an exception. Control does not jump through hidden runtime machinery. A function that may fail says so directly in its type.
Here is a small example:
const std = @import("std");
fn openFile() !void {
const file = try std.fs.cwd().openFile("data.txt", .{});
defer file.close();
std.debug.print("file opened\n", .{});
}
pub fn main() !void {
try openFile();
}The return type of openFile is:
!voidThis means:
either return
void, or return an error.
The ! forms an error union type. The value is either a normal result or an error.
The call:
try std.fs.cwd().openFile("data.txt", .{});means:
- call
openFile - if it succeeds, continue
- if it fails, return the error immediately
The compiler checks that errors are handled.
Suppose the file does not exist. The program prints something like:
error: FileNotFoundFileNotFound is an error value.
Error values belong to error sets.
An error set is written like this:
error{
FileNotFound,
AccessDenied,
OutOfMemory,
}This defines a set of named errors.
A function may return one of them:
const std = @import("std");
const ReadError = error{
EndOfStream,
InvalidData,
};
fn readNumber(ok: bool) ReadError!u32 {
if (!ok) {
return ReadError.InvalidData;
}
return 42;
}
pub fn main() !void {
const n = try readNumber(true);
std.debug.print("{d}\n", .{n});
}The return type:
ReadError!u32means:
either return a
u32, or one error fromReadError.
Errors are namespaced:
ReadError.InvalidDataThis avoids collisions between unrelated error names.
A function may return an error directly:
return ReadError.EndOfStream;or return a value:
return 42;The caller must deal with both possibilities.
Error sets can be inferred. In practice, many Zig programs use inferred errors for small internal functions and explicit error sets for library boundaries.
This function:
fn divide(a: u32, b: u32) !u32 {
if (b == 0) {
return error.DivisionByZero;
}
return a / b;
}creates an inferred error set containing:
error.DivisionByZeroThe caller may handle the error with catch:
const std = @import("std");
fn divide(a: u32, b: u32) !u32 {
if (b == 0) {
return error.DivisionByZero;
}
return a / b;
}
pub fn main() void {
const n = divide(10, 0) catch {
std.debug.print("cannot divide by zero\n", .{});
return;
};
std.debug.print("{d}\n", .{n});
}Errors are ordinary values. They are typed, checked by the compiler, and visible in function signatures.
This is one of the central ideas in Zig. A function says exactly how it may fail, and the caller decides what to do about it.
Exercise 8-1. Write a function that returns error.NegativeNumber if its argument is less than zero.
Exercise 8-2. Write a function that opens a file and returns error.EmptyName if the filename is empty.
Exercise 8-3. Modify divide so that it also returns error.Overflow.
Exercise 8-4. Write a program that reads a file name from the command line and prints a message if the file cannot be opened.