• Labs icon Lab
  • Core Tech
Labs

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.

Labs

Path Info

Level
Clock icon Intermediate
Duration
Clock icon 20m
Published
Clock icon Jun 09, 2025

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. 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 or zig 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.

  2. 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, where T 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 Errors

    The 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 Handling

    The 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 Statement

    When working with an error union, the idiomatic way to handle both success and error cases is to use an if expression with an else 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 the else branch. Inside the else branch, you can use a switch 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", .{}),
        }
    }
    

  3. 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 Set

    In this step, you will define a new error set called ReadConfigError that contains two possible errors: NotFound and InvalidValue.

    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 Type

    In this step, update readConfigValue to return an error union. The function should now return either a string or a ReadConfigError.

    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 from readConfigValue()

    Next, update readConfigValue to return specific ReadConfigError 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 Type

    Now 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 that value is assigned to be a ReadConfigError if that is applicable. The error you get now is due to the std.debug.print("Config value: {s}\n", .{value}); line in main 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 A try Block

    Now wrap the call to readConfigValue in a try 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 A catch Block And Early Return

    Now 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 an if and else Expression that Contains a switch Statement

    In this step, you will replace the try or catch block with an if expression and an else clause to handle each case explicitly.

    This will require removing the assignment of value and instead placing the call to readConfigValue directly inside the if expression. This allows you to unwrap the success value in the if branch, and handle any errors in the else branch.

    Inside the else branch, you will add a switch 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 the if branch when the call to readConfigValue 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
      

Jaecee is an associate author at Pluralsight helping to develop Hands-On content. Jaecee's background in Software Development and Data Management and Analysis. Jaecee holds a graduate degree from the University of Utah in Computer Science. She works on new content here at Pluralsight and is constantly learning.

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.