zig-utils/zig-error-handling
A type-safe error handling library for Zig inspired by Rust's Result<T, E> and TypeScript's neverthrow.
A type-safe error handling library for Zig inspired by Rust's Result<T, E> and TypeScript's neverthrow.
andThen, map, and morematch for elegant error handlingunwrap, unwrapOr, combine, and many more helperscollect, partition, and sequence for working with arrays of Resultsflatten, transpose, inspect for complex workflowstoErrorUnion()Add this library to your build.zig.zon:
.{
.name = "my-project",
.version = "0.1.0",
.dependencies = .{
.result = .{
.url = "https://github.com/yourusername/zig-result/archive/refs/tags/v0.1.0.tar.gz",
// Add hash after first fetch
},
},
}
Then in your build.zig:
const result = b.dependency("result", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("result", result.module("result"));
Or simply copy src/result.zig into your project.
const std = @import("std");
const Result = @import("result").Result;
fn divide(a: i32, b: i32) Result(i32, []const u8) {
if (b == 0) {
return Result(i32, []const u8).err("Division by zero");
}
return Result(i32, []const u8).ok(@divTrunc(a, b));
}
pub fn main() !void {
const result = divide(10, 2);
if (result.isOk()) {
std.debug.print("Result: {d}\n", .{result.unwrap()});
} else {
std.debug.print("Error: {s}\n", .{result.unwrapErr()});
}
}
// Create an Ok result
const success = Result(i32, []const u8).ok(42);
// Create an Err result
const failure = Result(i32, []const u8).err("Something went wrong");
// Check result status
if (success.isOk()) { /* ... */ }
if (failure.isErr()) { /* ... */ }
map - Transform success valuesconst result = Result(i32, []const u8).ok(21);
const doubled = result.map(i32, struct {
fn double(x: i32) i32 {
return x * 2;
}
}.double);
// doubled is Result(i32, []const u8).ok(42)
mapErr - Transform error valuesconst result = Result(i32, []const u8).err("failed");
const mapped = result.mapErr([]const u8, struct {
fn toUpper(s: []const u8) []const u8 {
return "FAILED";
}
}.toUpper);
// mapped is Result(i32, []const u8).err("FAILED")
andThen - Chain multiple fallible operationsfn parseNumber(str: []const u8) Result(i32, []const u8) {
const num = std.fmt.parseInt(i32, str, 10) catch {
return Result(i32, []const u8).err("Invalid number");
};
return Result(i32, []const u8).ok(num);
}
fn validatePositive(n: i32) Result(i32, []const u8) {
if (n <= 0) {
return Result(i32, []const u8).err("Number must be positive");
}
return Result(i32, []const u8).ok(n);
}
const result = parseNumber("42").andThen(i32, validatePositive);
// If parsing succeeds, validates the number
// If either fails, returns the error
orElse - Provide fallback on errorconst result = Result(i32, []const u8).err("failed");
const recovered = result.orElse(struct {
fn fallback(e: []const u8) Result(i32, []const u8) {
_ = e;
return Result(i32, []const u8).ok(0); // Default value
}
}.fallback);
// recovered is Result(i32, []const u8).ok(0)
const result = Result(i32, []const u8).ok(42);
const message = result.match([]const u8, .{
.ok = struct {
fn handleOk(x: i32) []const u8 {
return "Success!";
}
}.handleOk,
.err = struct {
fn handleErr(e: []const u8) []const u8 {
return e;
}
}.handleErr,
});
// Get value or panic
const value = result.unwrap();
// Get value or use default
const value = result.unwrapOr(0);
// Get value or compute from error
const value = result.unwrapOrElse(struct {
fn compute(e: []const u8) i32 {
return 0;
}
}.compute);
// Get value or panic with custom message
const value = result.expect("Expected a valid number");
const r1 = Result(i32, []const u8).ok(10);
const r2 = Result(i32, []const u8).ok(20);
const combined = r1.combine(r2);
// combined is Result(struct { i32, i32 }, []const u8).ok(.{ 10, 20 })
const values = combined.unwrap();
const sum = values[0] + values[1]; // 30
Convert between Zig's native error unions and Results:
const fromErrorUnion = @import("result").fromErrorUnion;
// Convert error union to Result
const errorUnion: anyerror!i32 = 42;
const result = fromErrorUnion(errorUnion);
// result is Result(i32, anyerror).ok(42)
// Convert Result to error union for safe unwrapping
const result = Result(i32, anyerror).ok(42);
const error_union = result.toErrorUnion(); // Returns anyerror!i32
const results = [_]Result(i32, []const u8){
Result(i32, []const u8).ok(1),
Result(i32, []const u8).ok(2),
Result(i32, []const u8).ok(3),
};
const collected = collect(i32, []const u8, allocator, &results);
// If all Ok: Result([]i32, []const u8).ok([1, 2, 3])
// If any Err: Returns first error
defer if (collected.isOk()) allocator.free(collected.unwrap());
const results = [_]Result(i32, []const u8){
Result(i32, []const u8).ok(1),
Result(i32, []const u8).err("error1"),
Result(i32, []const u8).ok(2),
};
const partitioned = try partition(i32, []const u8, allocator, &results);
defer allocator.free(partitioned.oks);
defer allocator.free(partitioned.errs);
// partitioned.oks = [1, 2]
// partitioned.errs = ["error1"]
const results = [_]Result(i32, []const u8){
Result(i32, []const u8).ok(10),
Result(i32, []const u8).ok(20),
};
const sequenced = sequence(i32, []const u8, allocator, &results);
// Stops at first error, otherwise returns all Ok values
defer if (sequenced.isOk()) allocator.free(sequenced.unwrap());
const result = Result(i32, []const u8).ok(42)
.inspect(struct {
fn log(val: i32) void {
std.debug.print("Value: {d}\n", .{val});
}
}.log)
.inspectErr(struct {
fn logErr(e: []const u8) void {
std.debug.print("Error: {s}\n", .{e});
}
}.logErr);
const transpose = @import("result").transpose;
const result_opt = Result(?i32, []const u8).ok(42);
const opt_result = transpose(i32, []const u8, result_opt);
// Some(Result(i32).ok(42))
const result_none = Result(?i32, []const u8).ok(null);
const none = transpose(i32, []const u8, result_none);
// null
const nested = Result(Result(i32, []const u8), []const u8).ok(
Result(i32, []const u8).ok(42)
);
const flattened = nested.flatten();
// Result(i32, []const u8).ok(42)
Result(T, E).ok(value: T) - Create a success resultResult(T, E).err(error: E) - Create an error resultisOk() bool - Returns true if OkisErr() bool - Returns true if Errmap(U, func: fn(T) U) Result(U, E) - Transform Ok valuemapErr(F, func: fn(E) F) Result(T, F) - Transform Err valuemapBoth(U, okFunc, errFunc) U - Transform both variantsandThen(U, func: fn(T) Result(U, E)) Result(U, E) - Chain operations (flatMap)orElse(func: fn(E) Self) Self - Provide fallback on errorandResult(other: Self) Self - Return other if Ok, self if ErrorResult(other: Self) Self - Return self if Ok, other if Errunwrap() T - Get Ok value or panicunwrapErr() E - Get Err value or panicunwrapOr(default: T) T - Get Ok value or defaultunwrapOrElse(func: fn(E) T) T - Get Ok value or compute from errorexpect(msg: []const u8) T - Get Ok value or panic with messageexpectErr(msg: []const u8) E - Get Err value or panic with messagematch(U, handlers: struct { ok: fn(T) U, err: fn(E) U }) U - Pattern match on resultcombine(other: Self) Result(struct { T, T }, E) - Combine two resultsokOrNull() ?T - Convert to optional, discarding errorerrOrNull() ?E - Convert to optional error, discarding valuefromErrorUnion(value: anytype) Result(...) - Convert error union to ResulttoErrorUnion() E!T - Convert Result to Zig error union for safe unwrappinginspect(func: fn(T) void) Self - Inspect Ok value without transformation (for logging/debugging)inspectErr(func: fn(E) void) Self - Inspect Err value without transformationflatten() Self - Flatten nested Result (Result(Result(T, E), E) -> Result(T, E))transpose(T, E, Result(?T, E)) ?Result(T, E) - Convert Result of Optional to Optional of Resultcollect(T, E, allocator, []Result(T, E)) Result([]T, E) - Transform slice of Results to Result of slicepartition(T, E, allocator, []Result(T, E)) {oks: []T, errs: []E} - Split Results into separate Ok and Err arrayssequence(T, E, allocator, []Result(T, E)) Result([]T, E) - Short-circuit on first error, collect all Ok valuesSee the examples/ directory for comprehensive examples.
Run the example:
zig build run-example
Run the test suite:
zig build test
Zig has built-in error unions (!T), which are great for simple cases. This library provides additional benefits:
| Feature | Error Union (!T) |
Result Type |
|---|---|---|
| Type safety | Error set only | Any error type |
| Explicit errors | No | Yes |
| Chainable | Limited | Yes |
| Pattern matching | Via catch |
Via match |
| Transform errors | Via catch |
Via mapErr |
| Combine results | Manual | combine |
Use error unions when:
try/catch syntaxUse Result when:
If you're coming from Rust or TypeScript:
| Rust | neverthrow | Zig Result |
|---|---|---|
Result::Ok(v) |
ok(v) |
Result(T, E).ok(v) |
Result::Err(e) |
err(e) |
Result(T, E).err(e) |
.map(f) |
.map(f) |
.map(U, f) |
.map_err(f) |
.mapErr(f) |
.mapErr(F, f) |
.and_then(f) |
.andThen(f) |
.andThen(U, f) |
.or_else(f) |
.orElse(f) |
.orElse(f) |
.unwrap() |
.unwrap() |
.unwrap() |
.unwrap_or(d) |
.unwrapOr(d) |
.unwrapOr(d) |
.match (via match) |
.match |
.match(U, handlers) |
MIT
Contributions are welcome! Please feel free to submit issues or pull requests.
Inspired by:
Result<T, E> type