Zig Functions
A function is a named block of code that performs a specific task. You write a function once and call it from many places in your program. Functions make code reusable, organized, and easier to test. Zig functions are explicit about what they accept, what they return, and whether they can fail.
Defining a Function
fn add(a: i32, b: i32) i32 {
return a + b;
}
fn add (a: i32, b: i32) i32 { return a + b; }
| | | | |
Key Name Parameters Return type Body
word
Call this function by writing its name with values in parentheses:
const result = add(10, 25);
std.debug.print("Sum: {d}\n", .{result});
// Output: Sum: 35
Public vs Private Functions
pub fn greet() void { ... } ← Visible to other files
fn helper() void { ... } ← Private to this file
Functions without pub are only accessible within the same file. Use pub only for functions that other modules need to call. Keeping most functions private reduces the surface area of your code — fewer things exposed means fewer things that can break when you make changes.
Functions That Return Nothing
fn printBanner(title: []const u8) void {
std.debug.print("=== {s} ===\n", .{title});
}
A function that performs an action without producing a value returns void. Calling it is enough — you do not assign the result to a variable.
Functions That Can Fail
When a function might encounter an error, prefix its return type with !:
fn divide(a: f64, b: f64) !f64 {
if (b == 0.0) return error.DivisionByZero;
return a / b;
}
Call site: const result = try divide(10.0, 2.0); // 5.0 const bad = try divide(10.0, 0.0); // panics — error propagated
divide(10, 2):
b = 2 ≠ 0 → return 5.0 ✓
divide(10, 0):
b = 0 → return error.DivisionByZero ✗
The try keyword before a call passes any error up to the caller automatically. The caller's return type must also include ! to accept errors. This chain continues until the error is either handled or reaches main.
Multiple Return Values via Structs
Zig functions return a single value, but that value can be a struct containing multiple fields:
const MinMax = struct { min: i32, max: i32 };
fn findMinMax(data: []const i32) MinMax {
var min = data[0];
var max = data[0];
for (data[1..]) |val| {
if (val < min) min = val;
if (val > max) max = val;
}
return MinMax{ .min = min, .max = max };
}
// Usage:
const nums = [_]i32{ 4, 1, 9, 2, 7 };
const result = findMinMax(&nums);
std.debug.print("Min: {d}, Max: {d}\n", .{result.min, result.max});
Input: [4, 1, 9, 2, 7]
|
findMinMax
|
MinMax {
min = 1,
max = 9
}
Function Parameters Are Immutable
Parameters behave like const — you cannot reassign them inside the function:
fn process(x: i32) void {
// x = x + 1; ← COMPILE ERROR
const y = x + 1; // Correct: make a new variable
_ = y;
}
To work with a modified copy, create a new local variable inside the function. This rule prevents functions from accidentally altering their inputs in ways callers do not expect.
Passing Arrays and Slices
Passing a full array (copies the array):
fn sum(data: [5]i32) i32 { ... }
Passing a slice (passes pointer + length, no copy):
fn sum(data: []const i32) i32 { ... }
Array copy model:
Caller: [1, 2, 3, 4, 5] → copy → Function sees: [1, 2, 3, 4, 5]
Slice model:
Caller: [1, 2, 3, 4, 5]
|
pointer + length
|
Function sees the same memory (no copy)
Prefer slices for arrays you pass to functions. Slices work for any array length, while typed arrays ([5]i32) only match arrays of exactly that size.
Recursive Functions
A function that calls itself is recursive. Recursion suits problems that naturally break into smaller versions of themselves:
fn factorial(n: u64) u64 {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
factorial(4):
4 * factorial(3)
3 * factorial(2)
2 * factorial(1)
= 1
= 2 * 1 = 2
= 3 * 2 = 6
= 4 * 6 = 24
Inline Functions
Marking a function inline asks the compiler to paste the function's code directly at each call site instead of jumping to a separate function. This eliminates function call overhead for very small functions used in performance-critical loops:
inline fn square(n: i32) i32 {
return n * n;
}
The Underscore — Discarding Return Values
When a function returns a value you do not need, assign it to _. Ignoring a return value without _ causes a compile error:
_ = someFunction(); // explicitly discard the result
This design means return values are never silently ignored. If a function signals that something happened through its return value, Zig forces you to acknowledge it — even if that acknowledgment is just discarding it intentionally with _.
Practical Example: Temperature Converter
const std = @import("std");
fn celsiusToFahrenheit(c: f64) f64 {
return (c * 9.0 / 5.0) + 32.0;
}
fn fahrenheitToCelsius(f: f64) f64 {
return (f - 32.0) * 5.0 / 9.0;
}
pub fn main() void {
const boiling_c: f64 = 100.0;
const body_f: f64 = 98.6;
std.debug.print("{d}°C = {d:.1}°F\n",
.{boiling_c, celsiusToFahrenheit(boiling_c)});
std.debug.print("{d}°F = {d:.1}°C\n",
.{body_f, fahrenheitToCelsius(body_f)});
}
Output:
100°C = 212.0°F 98.6°F = 37.0°C
