Skip to content

Returning a Struct

Many functions need to produce more than one piece of information.

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:

1920x1080

Anonymous 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:

!f64

means:

  • 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:

?i32

means:

  • 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 = 10

This 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.Allocator

The 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=25

This 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 structure

Instead 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:

PatternPurpose
Tsingle value
!Tvalue or error
?Tvalue or null
structmultiple related values
*Tindirect modification
[]Tmemory view

You will see these patterns repeatedly throughout Zig codebases.