Function
BasisPrerequisites
Every non-trivial program contains logic that needs to run from more than one place. Without a way to name and reuse that logic, you end up copying it — and every copy is a future bug waiting to diverge from the others. Functions solve this: they give a name to a block of logic so you can invoke it by name anywhere, any number of times, without repeating the code.
What is a function?
A function is a named, reusable piece of code that optionally accepts input values, runs a sequence of statements, and optionally produces an output value. You define the function once and call it — ask it to run — whenever you need that work done.
Think of a function like a labeled recipe card: the card’s title is the function’s name, the ingredient list is the parameter list, the steps are the body, and the dish you end up with is the return value.
Declaring a function
In Zig, a function declaration follows this form:
fn name(param1: Type1, param2: Type2) ReturnType {
// body
}
fnis the keyword that begins every function declaration.nameis the identifier you will use to call it. By convention, Zig function names usecamelCase.- The parameter list in parentheses declares the inputs. Each parameter has a name and a type, separated by a colon.
ReturnTypeis the type of the value the function produces.- The body is the block of statements between
{and}.
Here is a minimal concrete example:
fn add(a: i32, b: i32) i32 {
return a + b;
}
add takes two 32-bit signed integers and returns their sum. The return keyword hands the value back to the caller and exits the function immediately.
Zig requires an explicit
returnto produce a value from a function. Unlike some languages, the value of the last expression in a block is not automatically returned. If you omitreturn, the compiler will tell you that the function’s control flow can reach the end without returning a value.
The pub keyword
By default, a function is private — visible only within the file it is declared in. Adding pub before fn makes it public, so other files that import this one can call it:
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
A good rule of thumb: declare functions as private (no pub) unless there is a concrete reason for another file to call them. Keeping the public surface small makes a module easier to understand and refactor later.
The void return type
When a function does not need to produce a value — for example, one that only prints to the terminal — its return type is void. You may omit the return statement entirely, or write a bare return; to exit early:
const std = @import("std");
fn greet(name: []const u8) void {
std.debug.print("Hello, {s}!\n", .{name});
}
pub fn main() void {
greet("world"); // Hello, world!
}
void is not a value — it simply means “this function produces nothing useful.” Trying to assign the result of a void function to a variable is a compile error.
Calling a function
To call a function, write its name followed by arguments in parentheses, matching the order and types of the parameter list:
const std = @import("std");
fn square(n: i32) i32 {
return n * n;
}
pub fn main() void {
const result = square(7);
std.debug.print("{}\n", .{result}); // 49
}
The call square(7) hands the value 7 to n, runs the body, and the return sends 49 back. That value is then bound to result.
Function calls are expressions: their result can be passed directly to another call, used in arithmetic, or stored in a variable.
const std = @import("std");
fn double(n: i32) i32 {
return n * 2;
}
pub fn main() void {
std.debug.print("{}\n", .{double(double(3))}); // 12
}
The main function: the entry point
Every executable Zig program must define a main function. It is the first function the operating system calls when you run your program. All other functions are reachable only because main (or something main calls) eventually calls them.
const std = @import("std");
pub fn main() void {
std.debug.print("program started\n", .{});
}
main must be pub so the Zig build system can find it. It takes no parameters in its simplest form, and its return type is usually void (or !void when it can return errors — a pattern covered in the error handling checkpoint).
Parameters are immutable by default
Inside a function body, each parameter behaves like a const binding. You can read it, pass it further, or compute from it, but you cannot reassign it:
fn tryToMutate(x: i32) void {
x = x + 1; // compile error: cannot assign to constant
}
If you need a mutable local copy, declare a var inside the body and initialize it from the parameter:
fn countDown(start: i32) void {
const std = @import("std");
var n: i32 = start;
while (n > 0) : (n -= 1) {
std.debug.print("{}\n", .{n});
}
}
A complete working example
Here is a small program that ties the ideas together:
const std = @import("std");
fn clamp(value: i32, min: i32, max: i32) i32 {
if (value < min) return min;
if (value > max) return max;
return value;
}
fn printClamped(value: i32) void {
const result = clamp(value, 0, 100);
std.debug.print("clamp({}) = {}\n", .{ value, result });
}
pub fn main() void {
printClamped(-5); // clamp(-5) = 0
printClamped(50); // clamp(50) = 50
printClamped(120); // clamp(120) = 100
}
clamp encapsulates the boundary logic. printClamped wraps clamp and the printing into a single named operation. main reads as a short list of intentions. Each function does one thing.
Multiple return values
Zig functions return exactly one value. When you need to return more than one piece of data, the conventional approach is to return a struct that bundles those values together. Another common pattern is an error union (written ErrorSet!Type), which either returns a value or signals that something went wrong. Both patterns appear in later checkpoints; for now, just note that you are not limited to a single primitive as a return type.
comptime parameters
Functions in Zig can be made generic by declaring parameters with the keyword comptime, which tells the compiler to resolve that parameter’s value at compile time rather than at runtime. This unlocks type-level programming and zero-overhead abstractions. The full mechanics are covered in the comptime checkpoint; for now, it is enough to know that Zig’s generics work through this mechanism rather than a separate template or generic syntax.
Keeping functions small and focused
A function should do one thing and do it clearly. This principle — sometimes called the single responsibility idea — is not a rigid rule, but a guide: if you find yourself writing a long function with a comment like // --- part 2 --- in the middle, that is usually a sign to split it into two named functions.
Small, focused functions are:
- Easier to read — a good name summarizes what the function does, so callers can understand the code without reading the body.
- Easier to test — you can verify a small function in isolation.
- Easier to change — fixing or improving one piece of logic has no risk of breaking an unrelated piece in the same body.
There is no magic maximum line count. The test is: can you give this function a name that honestly describes everything it does?
Summary
- A function names a reusable block of logic. Declare one with
fn name(params) ReturnType { }. pubmakes a function visible to other files. Omit it to keep a function private to its own file.- Parameters are declared as
name: Typepairs and are immutable inside the body. Declare a localvarif you need a mutable copy. - Use
return value;to produce a result. Zig does not implicitly return the last expression —returnis always required. - When a function produces nothing useful, its return type is
voidandreturnmay be omitted. mainis the program’s entry point. It must bepuband takes no parameters in its basic form.- To return multiple values, use a struct; to return a value-or-error, use an error union — both are covered in later checkpoints.
- Functions can accept
comptimeparameters to be generic; the full story is in the comptime checkpoint. - Keep functions small and focused: one clear responsibility per function makes code easier to read, test, and change.