Multiple Return Patterns
Many functions need to produce more than one piece of information.
For example:
- a parser may return both a value and a position
- a file loader may return data and metadata
- a math function may return multiple coordinates
- a search function may return a result and a status
Some languages support multiple return values directly.
Zig takes a simpler approach.
Instead of special syntax, Zig usually uses:
- structs
- error unions
- optionals
- pointers to output values
This keeps the language small and predictable.
Returning a Struct
The most common pattern is returning a struct.
Example:
const Point = struct {
x: f64,
y: f64,
};
fn createPoint() Point {
return Point{
.x = 10.5,
.y = 20.0,
};
}Calling:
const p = createPoint();Using the result:
std.debug.print(
"({}, {})\n",
.{ p.x, p.y },
);Output:
(10.5, 20)This is Zig’s preferred style.
A struct groups related data together clearly.
Anonymous Struct Returns
Sometimes you only need a temporary structure.
Example:
fn getSize() struct { width: u32, height: u32 } {
return .{
.width = 1920,
.height = 1080,
};
}Calling:
const size = getSize();Using it:
std.debug.print(
"{}x{}\n",
.{ size.width, size.height },
);Output:
1920x1080Anonymous structs are useful for small grouped results.
Returning Success or Failure
One of the most important Zig patterns is returning either:
- a value
- or an error
Example:
fn divide(a: f64, b: f64) !f64 {
if (b == 0) {
return error.DivisionByZero;
}
return a / b;
}The return type:
!f64means:
- success →
f64 - failure → error
Calling:
const result = try divide(10, 2);This pattern is everywhere in Zig.
Unlike exceptions, errors are visible directly in the type system.
Returning Optional Values
Sometimes a function may not find a result.
Instead of throwing an error, Zig often uses optionals.
Example:
fn findEven(value: i32) ?i32 {
if (value % 2 == 0) {
return value;
}
return null;
}The type:
?i32means:
- either an
i32 - or
null
Calling:
const result = findEven(10);Checking:
if (result) |value| {
std.debug.print("{}\n", .{value});
} else {
std.debug.print("not found\n", .{});
}Optionals are useful when “no result” is normal behavior.
Combining Errors and Optionals
Functions may combine both ideas.
Example:
fn parseNumber(text: []const u8) !?i32 {
}Meaning:
- the function may fail with an error
- the function may return
null - the function may return an integer
This can look complicated initially, but it becomes natural with practice.
Output Parameters
Sometimes functions write results into caller-provided memory.
Example:
fn swap(a: *i32, b: *i32) void {
const temp = a.*;
a.* = b.*;
b.* = temp;
}Calling:
var x: i32 = 10;
var y: i32 = 20;
swap(&x, &y);After execution:
x = 20
y = 10This pattern is common in systems programming because it avoids unnecessary copying.
Returning Large Data
Returning large structures repeatedly may be expensive.
Example:
const BigData = struct {
values: [10000]u32,
};Instead of returning copies, programs often use pointers or allocators.
Example:
fn process(data: *BigData) void {
}This allows the function to work directly with existing memory.
Returning Slices
Functions often return slices.
Example:
fn firstThree(values: []const i32) []const i32 {
return values[0..3];
}Calling:
const data = [_]i32{ 1, 2, 3, 4, 5 };
const result = firstThree(&data);Result:
[1, 2, 3]Slices are lightweight views into memory.
No copying happens here.
Returning Allocated Memory
Sometimes functions allocate memory dynamically.
Example:
fn createBuffer(
allocator: std.mem.Allocator,
) ![]u8 {
return try allocator.alloc(u8, 1024);
}This pattern is extremely important in Zig.
Notice the allocator parameter:
allocator: std.mem.AllocatorThe caller controls memory allocation.
This makes ownership explicit.
Ownership and Lifetime
When returning memory-related values, lifetime matters.
Safe example:
fn getMessage() []const u8 {
return "hello";
}String literals live for the entire program.
Dangerous example:
fn badSlice() []u8 {
var data = [_]u8{ 1, 2, 3 };
return &data;
}This is invalid because data disappears when the function exits.
Returning references to dead memory is a serious bug.
Zig tries hard to detect these mistakes.
Tagged Union Returns
Sometimes functions may produce one of several result types.
Example:
const Result = union(enum) {
integer: i32,
text: []const u8,
};
fn example(flag: bool) Result {
if (flag) {
return .{ .integer = 123 };
}
return .{ .text = "hello" };
}This pattern is useful when results vary in shape.
You will learn tagged unions later in depth.
Returning State Objects
Functions often return configuration or state structures.
Example:
const Config = struct {
width: u32,
height: u32,
fullscreen: bool,
};
fn defaultConfig() Config {
return .{
.width = 1280,
.height = 720,
.fullscreen = false,
};
}This style is common in real Zig programs.
Returning Iterators
Some APIs return iterator objects.
Example conceptually:
const iterator = list.iterator();The iterator itself stores traversal state.
This avoids returning large collections repeatedly.
A Complete Example
const std = @import("std");
const Stats = struct {
min: i32,
max: i32,
sum: i32,
};
fn analyze(values: []const i32) Stats {
var min = values[0];
var max = values[0];
var sum: i32 = 0;
for (values) |value| {
if (value < min) {
min = value;
}
if (value > max) {
max = value;
}
sum += value;
}
return .{
.min = min,
.max = max,
.sum = sum,
};
}
pub fn main() void {
const values = [_]i32{ 4, 8, 1, 9, 3 };
const stats = analyze(&values);
std.debug.print(
"min={}, max={}, sum={}\n",
.{
stats.min,
stats.max,
stats.sum,
},
);
}Output:
min=1, max=9, sum=25This demonstrates a common Zig pattern:
- compute several related values
- return them together inside a struct
Mental Model
In Zig, “multiple return values” usually means:
group related data into a structureInstead of special syntax, Zig uses normal language features consistently.
This keeps the language simpler and easier to reason about.
The most important return patterns in Zig are:
| Pattern | Purpose |
|---|---|
T | single value |
!T | value or error |
?T | value or null |
struct | multiple related values |
*T | indirect modification |
[]T | memory view |
You will see these patterns repeatedly throughout Zig codebases.