Skip to content

The `return` Keyword

Functions often need to produce results.

Return Values

Functions often need to produce results.

A function may calculate something, create data, search for information, or transform input into output. The result comes back to the caller through a return value.

A simple example:

fn add(a: i32, b: i32) i32 {
    return a + b;
}

This function:

  • receives two integers
  • adds them
  • returns the result

The return type appears after the parameter list:

i32

meaning the function returns a signed 32-bit integer.

The return Keyword

The return keyword immediately exits the function and sends a value back to the caller.

Example:

fn square(x: i32) i32 {
    return x * x;
}

Calling it:

const result = square(5);

After execution:

result = 25

The function runs:

x * x

then returns the computed value.

Understanding Function Flow

When a function is called, execution temporarily jumps into the function.

Example:

fn multiply(a: i32, b: i32) i32 {
    return a * b;
}

Call:

const value = multiply(3, 4);

Flow:

main
  -> multiply(3, 4)
       -> return 12
  <- back to main

The returned value becomes the result of the function call.

Returning Different Types

Functions can return many kinds of values.

Returning Integers

fn getScore() i32 {
    return 100;
}

Returning Floating Point Values

fn pi() f64 {
    return 3.1415926535;
}

Returning Booleans

fn isEven(value: i32) bool {
    return value % 2 == 0;
}

Calling:

const result = isEven(10);

Result:

true

Returning Strings

Strings are usually slices.

fn getName() []const u8 {
    return "Zig";
}

Calling:

const name = getName();

Returning Structs

Functions can return complex data structures.

Example:

const Point = struct {
    x: i32,
    y: i32,
};

fn createPoint() Point {
    return Point{
        .x = 10,
        .y = 20,
    };
}

Calling:

const p = createPoint();

Now:

p.x = 10
p.y = 20

This is common in Zig programs.

Returning Arrays

Functions can return arrays.

fn makeArray() [3]i32 {
    return [3]i32{ 1, 2, 3 };
}

Calling:

const numbers = makeArray();

Result:

[1, 2, 3]

Returning Early

return exits immediately.

Example:

fn check(value: i32) i32 {
    if (value < 0) {
        return 0;
    }

    return value;
}

Calling:

check(-5);

Flow:

value < 0
return 0

The second return never executes.

This pattern is extremely common.

Multiple Return Paths

Functions may return from different branches.

Example:

fn max(a: i32, b: i32) i32 {
    if (a > b) {
        return a;
    }

    return b;
}

Calling:

const result = max(10, 30);

Result:

30

Each path must return the correct type.

Void Return Type

Some functions do not return values.

These use void.

Example:

fn printMessage() void {
    std.debug.print("Hello\n", .{});
}

The function performs an action but returns nothing.

Calling:

printMessage();

You cannot store the result because there is no result.

Incorrect:

const x = printMessage();

Implicit vs Explicit Returns

Zig requires explicit return.

This is invalid:

fn add(a: i32, b: i32) i32 {
    a + b;
}

Correct:

fn add(a: i32, b: i32) i32 {
    return a + b;
}

This design makes control flow easier to read.

Nothing is hidden.

Returning Error Unions

Many Zig functions may fail.

These functions return error unions.

Example:

fn divide(a: f64, b: f64) !f64 {
    if (b == 0) {
        return error.DivisionByZero;
    }

    return a / b;
}

The return type:

!f64

means:

  • either an f64
  • or an error

Calling:

const result = try divide(10, 2);

This topic becomes very important later in Zig.

Returning Optionals

Functions may also return optional values.

Example:

fn find(value: i32) ?i32 {
    if (value == 10) {
        return 10;
    }

    return null;
}

The type:

?i32

means:

  • either an i32
  • or null

Optionals are Zig’s way of representing “maybe there is a value.”

Named Result Variables

Zig also allows named return variables.

Example:

fn add(a: i32, b: i32) i32 {
    const result = a + b;

    return result;
}

This can improve readability when computations are more complex.

Returning Pointers

Functions may return pointers.

Example:

fn getPointer(value: *i32) *i32 {
    return value;
}

But pointer lifetimes matter.

Dangerous example:

fn badPointer() *i32 {
    var x: i32 = 10;

    return &x;
}

This is invalid because x disappears after the function ends.

Returning pointers safely is an important topic in systems programming.

Return Type Checking

Zig verifies returned values carefully.

Incorrect example:

fn test() i32 {
    return "hello";
}

Compiler error:

expected i32, found string

The compiler prevents invalid return types before execution.

A Complete Example

const std = @import("std");

fn calculateArea(width: f64, height: f64) f64 {
    return width * height;
}

fn describe(area: f64) []const u8 {
    if (area > 100.0) {
        return "large";
    }

    return "small";
}

pub fn main() void {
    const area = calculateArea(12.0, 9.0);

    const text = describe(area);

    std.debug.print(
        "Area: {}, Type: {s}\n",
        .{ area, text },
    );
}

Output:

Area: 108, Type: large

This program demonstrates several important ideas:

  • returning numbers
  • returning strings
  • returning from branches
  • passing returned values into other functions

Functions become much more powerful once they can produce results.

Mental Model

You can think of a function as a transformation.

Input enters:

parameters

processing happens:

computation

output leaves:

return value

This pattern appears everywhere in programming.

A large software system is often just thousands of small transformations connected together.