Skip to main content

Error Handling

Zynx uses a minimal, explicit error model based on error sets and error unions.
This section describes how to declare errors, propagate them, and handle them explicitly in your code.

For a broader view of the type system, see Type System.
For async error behavior, see Async / Await.

Error Sets

An error set is a named collection of error codes.

error FileError {
NotFound,
AccessDenied,
DiskFull
}

Error Unions (T!)

An error union is a type that can hold either a value of type T or an error code.

fn open_file(path: str) -> i32! {
if (path == "") { return FileError.NotFound; }
return 1;
}

Error variants can carry an optional message string:

fn open_file(path: str) -> i32! {
if (path == "") { return FileError.NotFound("path is empty"); }
return 1;
}

The message is a str and can be retrieved in a catch (err, msg) handler.

Cancellation in async code is also expressed through T!. A canceled Future<T!> resolves to an error value (for example AsyncError.Cancelled) and must be handled explicitly with try or catch.

For details, see Async / Await.

The try Operator

Use try to propagate errors to the calling function. The caller must also return an error union.

fn process_file() -> i32! {
let fd = try open_file("data.txt");
return fd + 1;
}

The catch Operator

Use catch to handle errors locally. It provides a fallback value, an error handler block, or a handler expression.

Fallback

let fd = open_file("config.txt") catch -1;

In this form, if open_file returns an error, fd is initialized to -1.
Use this pattern when a simple, local default is acceptable.

Handler

Inside the catch block, use (err) to access the error code.

let fd = open_file("config.txt") catch (err) {
os.write("Error: {err}\n");
return -1;
};

Handler with Message

Use (err, msg) to also bind the error message string as an immutable str. When no message was attached, msg defaults to "unknown error".

fail() catch (err, msg) {
os.write("err: {err}\n");
os.write("msg: {msg}\n");
};

Captured Handler Expression

When you only need one expression, you can capture the error and return directly:

let fd = open_file("config.txt") catch (err) err_default(err);

This is equivalent to a block form but shorter.

Runtime Module Loading Errors (require)

require("...") is typed and participates in error handling. Use try or catch.

const math = try require("plugins/libmath.so");
let x = try math.add(10, 20);

If you want local recovery:

const math = require("plugins/libmath.so") catch null;

require() itself returns an error union, and catch (err) on that expression binds err: str:

const msg: str = require("plugins/libmath.so") catch (err) err;

Dynamic member access is also fallible because symbol resolution is lazy:

const math = try require("plugins/libmath.so");
let answer = math.ANSWER catch 0;
let sum = math.add(10, 20) catch 0;

When the exported ABI type is plain T, the dynamic expression behaves as T! so missing symbols can be handled through the same try / catch flow.

On dynamic loader failures, err receives the loader message captured by the runtime.

Unwrapping with if

Checking an error union in an if condition automatically unwraps the value in the then block.

let res: i32! = open_file("data.txt");
if res {
// res is now a shadowed i32 here
os.write("Success: {res}\n");
}

In the then branch, res is treated as the success value (i32 in this example).
In the else branch, res is an error from the corresponding error set.

main with Error Return

main may return void! to propagate errors out of the program entry point. If main returns an error, the runtime prints a diagnostic message to stderr and exits with code 1.

error Err {
Example
}

fn main() -> void! {
try throw();
}

Output on error:

error(Err::Example): unhandled error: `Err::Example`

Use try inside main to propagate errors from called functions, or handle them with catch to control the exit behavior manually.


  • Use error sets to define named groups of error codes.
  • Use error unions (T!) to return either a value or an error explicitly.
  • Attach a message to an error variant: ErrorSet.Variant("message").
  • Use try to propagate errors and catch to handle them locally.
  • Use catch (err, msg) to bind both the error code and its message string.
  • Use if conditions on error unions to unwrap success values in a structured way.
  • require("...") integrates with the same try / catch flow.
  • main may return void!; unhandled errors print a diagnostic and exit with code 1.

Related documentation: