- Lab
- Core Tech

Practice: Error Handling in Zig
Master Zig’s powerful error handling system in this hands-on lab designed for speed and clarity. In just 20 minutes, you’ll gain practical experience with Zig’s unique approach to managing errors—without the clutter of exceptions or complex stack traces. Learn how to define error sets, use error unions, and implement clean error handling strategies that make your code safer and more robust. Whether you're new to Zig or sharpening your skills, this practice lab gives you the essential tools to confidently write and reason about error-resilient programs.

Path Info
Table of Contents
-
Challenge
Introduction
Welcome to the Practice: Error Handling in Zig Lab
Welcome to the Practice: Error Handling in Zig lab! In this lab, you will explore how to implement robust and clear error handling using Zig’s built-in features.
By the end of this lab, you will be able to:
- Define and use error sets to represent known errors in your program
- Use error unions to allow functions to return either a value or an error
- Apply try, catch, and switch blocks to handle errors safely and cleanly
- Build a program that fails gracefully when invalid input is provided, rather than crashing
Why is this important? Zig’s approach to error handling makes errors explicit and compile-time checked, helping you write safe and predictable code. You will learn how to use these mechanisms step-by-step as you improve a small configuration-reading program.
Analyzing the Problem
In this scenario, you are working with a small program that defines a
readConfigValue
function. This function simulates looking up a configuration value based on a provided key and printing the result.Currently, the program does not implement any error handling. If you run the program with a key other than
"username"
, the program panics and fails ungracefully.You can run the program from the command line like this:
zig run main.zig -- <insert config key to get value for>
For example:
zig run main.zig -- username
will return "Alice".However, running
zig run main.zig -- password
orzig run main.zig -- anyOtherKey
will cause the program to panic.The goal of this lab is to refactor the program so that it does not crash. Instead, it should handle errors cleanly, with specific errors for cases where a key is not found or is inaccessible (such as the "password" key, which we do not want to display).
Take a moment now to run the program with different keys and observe the current behavior.
Addressing the Problem
Open
main.zig
. You will see that the program currently throws an error when an unrecognized key is passed, and there is no error handling in place.To resolve this issue, you will implement proper error handling using Zig’s error sets, error unions, and error-handling constructs. Your goal will be to make the program respond gracefully without crashing.
In the next step, you will be guided through adding error handling to the program.
info> As in other code labs, you will find a
solution
folder provided in the filetree. You can refer to this solution folder if you want to compare your task implementation or if you need help at any point during a task. -
Challenge
Error Handling in Zig Basics
Basics of Error Handling in Zig
Zig takes a simple and explicit approach to error handling. Instead of using exceptions or hidden control flow, Zig uses error unions to represent operations that may fail. The compiler forces you to handle all possible errors, either by propagating them with try or by handling them directly with catch or switch.
Zig’s error handling is designed to be:
- explicit
- compile-time checked
- zero-cost at runtime (no hidden stack unwinding)
In this section, you will learn about the key concepts of Zig’s error handling: error sets, error unions, try, catch, and switch blocks.
Error Sets
Error sets in Zig define a list of possible error conditions that your program can produce. They help make error handling explicit and allow the compiler to check for unhandled errors.
Example:
const ReadConfigError = error{ NotFound, InvalidValue, };
Error Unions
An error union allows a function to return either an error or a success value. The syntax for an error union is
!T
, whereT
is the success type.Example:
fn readConfigValue(key: []const u8) ReadConfigError![]const u8 { ... }
When you write
ReadConfigError![]const u8
as a return type, you are saying the following:This is an error union where:
- The error set is exactly
ReadConfigError
- The success type is
[]const u8
If you do not specify the exact error set, Zig uses default error sets.
try
and Propagating ErrorsThe
try
keyword can be used to call a function that returns an error union. If the call succeeds, the result is returned. If the call fails, the error is propagated up to the caller.Example:
const value = try readConfigValue(key);
catch
Blocks for Error HandlingThe
catch
keyword allows you to handle an error locally instead of propagating it. You can provide a block of code to run when an error occurs.Example:
const value = readConfigValue(key) catch |err| { std.debug.print("Caught error: {}\n", .{err}); return; };
Handling Error Unions with an
if
/else
Statement and Switch StatementWhen working with an error union, the idiomatic way to handle both success and error cases is to use an
if
expression with anelse
clause. This pattern works with any type of success value, including slices and complex types.If the operation succeeds, the value is unwrapped in the
if
branch. If an error occurs, it is handled in theelse
branch. Inside theelse
branch, you can use aswitch
statement to decipher which error message to print depending on the type of error.Example:
if (readConfigValue(key)) |value| { std.debug.print("Config value: {s}\n", .{value}); } else |err| { switch (err) { error.NotFound => std.debug.print("Key not found\n", .{}), error.InvalidValue => std.debug.print("Access denied: cannot display password\n", .{}), } }
-
Challenge
Implementing Error Handling in Zig
Implementing Error Handling Around
readConfigValue()
As you go, run the following commands from the Terminal window in the
workspace
directory to test out your changes:zig run main.zig -- username zig run main.zig -- password zig run main.zig -- unknownKey
You should try to accomplish each of the following tasks with the information provided in the earlier section on error handling in Zig.
Task 1: Define the
ReadConfigError
Error SetIn this step, you will define a new error set called
ReadConfigError
that contains two possible errors:NotFound
andInvalidValue
.Example Solution
const ReadConfigError = error{ NotFound, InvalidValue, };
After making this change, running the program should behave the same as before, since the error set is not yet used.
- Example output if you run with the username key:
Config value: Alice
- Or, if running with password or unknownKey:
thread 1234 panic: Unknown config key!
Task 2: Add An Error Union to
readConfigValue
’s Return TypeIn this step, update
readConfigValue
to return an error union. The function should now return either astring
or aReadConfigError
.Example Solution
Change the return type to:
fn readConfigValue(key: []const u8) ReadConfigError![]const u8 { ... }
At this point, because
main
is not handling the error union, the program will fail to compile or crash when you run it.- Example output from a command line run of the program with any configuration key:
error: cannot format error union without a specifier (i.e. {!} or {any}) @compileError("cannot format error union without a specifier (i.e. {!} or {any})");
Task 3: Return
ReadConfigError
Values fromreadConfigValue()
Next, update
readConfigValue
to return specificReadConfigError
values depending on the key.Example Solution
if (std.mem.eql(u8, key, "username")) { return "Alice"; } else if (std.mem.eql(u8, key, "password")) { return ReadConfigError.InvalidValue; } else { return ReadConfigError.NotFound; }
At this point, the program will crash when an error is returned because
main
is not yet handling the error.- Example output from a command line run of the program with any configuration key:
error: cannot format error union without a specifier (i.e. {!} or {any}) @compileError("cannot format error union without a specifier (i.e. {!} or {any})");
Task 4 Part 1: Add An Error Union To
main
's Return TypeNow update
main
to be able to propagate errors.Example Solution
Change its declaration to:
pub fn main() !void {
The program will still crash with any key other than
"username"
, but now this is expected and matches Zig’s error propagation model. This means thatvalue
is assigned to be aReadConfigError
if that is applicable. The error you get now is due to thestd.debug.print("Config value: {s}\n", .{value});
line inmain
that tries to print a string which is not the correct format for an error union.error: cannot format error union without a specifier (i.e. {!} or {any}) @compileError("cannot format error union without a specifier (i.e. {!} or {any})");
Task 4 Part 2: Wrap The Call To
readConfigValue()
In Atry
BlockNow wrap the call to
readConfigValue
in atry
block:Example Solution
const value = try readConfigValue(key);
The program behavior is now consistent with Zig’s model: if an error is returned, it will be propagated and printed by Zig’s runtime.
- Example output when running with password:
thread 1234 in readConfigValue (main): return ReadConfigError.InvalidValue; thread 1234 in main (main): const value = try readConfigValue(key);
Task 5: Handle Errors From The Call To
readConfigValue()
With Acatch
Block And Early ReturnNow add a
catch
block to handle errors gracefully.Example Solution
const value = readConfigValue(key) catch |err| { std.debug.print("Caught error: {}\n", .{err}); return; };
Now, instead of crashing, the program will catch the error and print it.
- Example output when running with password:
Caught error: error.InvalidValue
- Example output when running with unknownKey:
Caught error: error.NotFound
Task 6: Process
readConfigValue()
's Result With anif
andelse
Expression that Contains aswitch
StatementIn this step, you will replace the
try
orcatch
block with anif
expression and anelse
clause to handle each case explicitly.This will require removing the assignment of
value
and instead placing the call toreadConfigValue
directly inside theif
expression. This allows you to unwrap the success value in theif
branch, and handle any errors in theelse
branch.Inside the
else
branch, you will add aswitch
statement to match each possible error and print an appropriate message for each one.Also, remove the final call to print the config value at the bottom of
main
. The value will now be printed in theif
branch when the call toreadConfigValue
is successful.Example Solution
if (readConfigValue(key)) |value| { std.debug.print("Config value: {s}\n", .{value}); } else |err| { switch (err) { error.NotFound => std.debug.print("Key not found\n", .{}), error.InvalidValue => std.debug.print("Access denied: cannot display password\n", .{}), } }
- Example output with username:
Config value: Alice
- Example output with password:
Access denied: cannot display password
- Example output with unknownKey:
Key not found
- Example output if you run with the username key:
What's a lab?
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Provided environment for hands-on practice
We will provide the credentials and environment necessary for you to practice right within your browser.
Guided walkthrough
Follow along with the author’s guided walkthrough and build something new in your provided environment!
Did you know?
On average, you retain 75% more of your learning if you get time for practice.