Introduction to Zig

Updated: 05 June 2026

The Main Function

Zig functions are private by default. Making a function public can be done using pub

A main function must be public and looks like this:

1
pub fn main() void {
2
// do stuff
3
}

Imports

Imports are done using the @import built-in function. The Zig standard library can be imported using the @import function as such:

1
const std = @import("std");

Variables

Zig uses const to define constants and var to variables. These declarations can also have types associated with them though these can also be inferred for constants:

1
var a: u8 = 50;
2
const b: u8 = 50;
3
const c = -11;

Arrays

Arrays have a fixed length which can either be declared or inferred. Declaring an array inline looks like this:

1
var data: [3]u32 = [3]u32{ 1, 2, 3 };

Inferring the type and the size can be done like so:

1
var data = [_]u32{ 1, 2, 3 };

Accessing a value from an array can be done using index notation:

1
const x = data[1];
2
data[2] = x;

Arrays can be concatenated using ++:

1
const a = [_]u8{ 1, 2 };
2
const b = [_]u8{ 3, 4 };
3
4
const c = a ++ b;

++ is a comptime operator and runs during compilation

Declaring an array without assigning any values can be done using undefined:

1
var data: [3]u8 = undefined;

Strings

Zig stores strings as an array of bytes. Strings are denoted using double quotes (")

1
const str = "hello";
2
const h = str[0];

The ++ operator can also be used to concatenate strings

1
const result = "hello" ++ " " ++ "world";

Multiline strings can also be defined. These use \\ at the start of each line with no surrounding quotes:

1
const multiline =
2
\\First line of string
3
\\Second line of string
4
;

If/Else

If/else statements in Zig work as you’d expect:

1
const a = 10;
2
3
if (a > 5) {
4
std.debug.print("is big\n", .{});
5
} else {
6
std.debug.print("is small\n", .{});
7
}

Zig also has If/else expressions which can be used like so:

1
const a = 10;
2
const price = if (a > 5) "high" else "low";

While Loops

The syntax for a while loop is also pretty normal looking:

1
var a = 10;
2
while (a > 5) {
3
a -= 1;
4
}

Loops also have an optional “continue expression” which is run at the end of the loop or whenever a continue is invoked:

1
var a = 5;
2
while (a < 10) : (a += 1) {
3
std.debug.print("{}\n", .{a})
4
}

Or:

1
var a = 5;
2
while (a < 10) : (a += 1) {
3
// this will not print but will run the continue expression
4
if (a == 7) continue;
5
std.debug.print("{}\n", .{a})
6
}

Zig also has break statements that exit the loop immediately, for example:

1
var a = 5;
2
while (a < 10) : (a += 1) {
3
// this will exit the loop
4
if (a == 7) break;
5
std.debug.print("{}\n", .{a})
6
}

For Loops

For loops iterate over arrays and have the following structure:

1
const arr = [_]{ 1, 2, 3 };
2
3
for (arr) |item| {
4
// do stuff with item
5
}

Additionally, they also support the ability to use a provided indexer:

1
const arr = [_]{ 1, 2, 3 };
2
3
for (arr, 0..) |item, i| {
4
// do stuff with item and index
5
}

Functions

Functions are defined using the fn keyword along with a return type:

1
fn doThing() u8 {
2
return 10;
3
}

Functions can also take parameters:

1
fn times2(n: u8) u16 {
2
return n * 2;
3
}

Errors

Errors are values in Zig. Defining a set of errors can be done like so:

1
const MyCustomError = error {
2
MyErrorA,
3
MyErrorB,
4
MyErrorC,
5
};

A function can return an error as a normal value like so:

1
fn myError(n: u8) MyCustomError {
2
if (n == 0) return MyCustomError.MyErrorA;
3
if (n == 1) return MyCustomError.MyErrorB;
4
5
return MyCustomError.MyErrorC;
6
}

And then be handled by comparing the values:

1
const result = myError(x);
2
3
if (x == MyCustomError.MyErrorA) {
4
// do stuff
5
}

Additionally, values can also be a type of an “error union” in which they can represent a value or an error:

1
// this can hold an error or a number
2
const myValue: MyCustomError!u8 = 5;

We can define a function that returns an error union like:

1
fn doThing(n: u8) MyCustomError!u8 {
2
if (n == 0) return MyCustomError.MyErrorA;
3
4
return n;
5
}

And we can use it with a catch to provide a fallback value:

1
// result will be 0 if doThing returns an error
2
const result: u8 = doThing(x) catch 0;

catch also can capture the error and do something with it:

1
const result: u8 = doThing(x) catch |err| {
2
if (err == MyCustomError.MyErrorA) {
3
return 10;
4
}
5
6
return 0;
7
}

Zig also has try:

1
const result = try doThing();

Which is basically shorthand for the following:

1
const result = doThing() catch |err| return err;

Zig can also infer an error by leaving out the type of error and specifying the !resultType but this isn’t recommended for any function other than main which can return void or fail fn main() !void

Defer

The defer keyword will run some code after a block exits:

1
pub fn main() void {
2
// this will get run last
3
defer std.debug.print("End of function\n", .{});
4
5
std.debug.print("Start of function\n', .{});
6
}

Note that if there are multiple defers

There is also errdefer which runs if a block exits with an error:

1
errdefer cleanup();
2
const result = try doThing();

Switch

Switch statements must be exhaustive or have an else branch. This looks like so:

1
pub fn doThing(num: u8) u8 {
2
switch (num) {
3
1 => return 'a',
4
2 => return 'b',
5
3 => return 'c',
6
4 => return 'd',
7
else => return 'x',
8
}
9
}

Switches can also be used as an expression, like so:

1
pub fn doThing(num: u8) u8 {
2
const result = switch (num) {
3
1 => 'a',
4
2 => 'b',
5
3 => 'c',
6
4 => 'd',
7
else => 'x',
8
};
9
10
return result;
11
}

Unreachable

Zig also has an unreachable statement that lets us tell the compiler that a branch should never be executed and that hitting it would be an error:

1
// num is only ever 1 or 2
2
const result = switch (num) {
3
1 => 'a',
4
2 => 'b',
5
else => unreachable,
6
};

If/Else/Error

When working with errors, it’s also possible to use an error-handling version of an if statement that looks like so:

1
if (valOrErr) |val| {
2
// do things with val
3
} else |err| {
4
// do things with err
5
}

This can also be combined with a switch to handle specific types of errors:

1
if (valOrErr) |val| {
2
// do things with val
3
} else |err| switch (err) {
4
MyCustomError.MyErrorA => {
5
// handle error
6
},
7
MyCustomError.MyErrorB => {
8
// handle error
9
},
10
}

Enums

Enums are values that can be one of a specific list. Defining an enum looks like so:

1
const MyEnum = enum {
2
valA,
3
valB,
4
valC,
5
};

Enums are just numbers and so we can assign actual values to them as well:

1
const MyEnum = enum {
2
valA = 1,
3
valB = 3,
4
valC = 5,
5
};

Enums can be converted to integers using @intFromEnum(x)

Structs

The syntax to define a struct looks like so:

1
const MyStruct = struct {
2
enu: MyEnum,
3
val: u8,
4
};

Creating an instance of a struct can be done using the following syntax:

1
const thing = MyStruct {
2
.enu = MyEnum.ValA,
3
.val = 10,
4
};

Structs properties can also accessed and modified:

1
var thing = MyStruct {
2
.enu = MyEnum.ValA,
3
.val = 10,
4
};
5
6
thing.val = 20;

Pointers

You can create a pointer to a value can be created with & and pointers can be dereferenced with name.* The type of a pointer for type is *type:

1
var a: u8 = 1;
2
const a_ptr: *u8 = &a;
3
4
const b: u8 = a_ptr.*;

You can make const pointers to var values but you cannot make var pointers to const values

Zig also lets you define constant pointers which can point at constants or variables:

1
var a: u8 = 1;
2
const a_ptr: *const u8 = &a;
3
4
const b: u8 = 1;
5
const b_prt: *const u8 = &b;

Pointers let us pass values by reference, for example updating the passed variable:

1
fn update(x: *u8, val: u8) void {
2
x.* = val;
3
}

Note that if we’ve got a pointer to a struct we can still access the members directly without having to dereference first: x.inner instead of x.*.inner

Optionals

Optional values may have data or null and are typed using the ?:

1
var data: ?u8 = 10;

Optional values can be defaulted using orelse:

1
var definitely_data: u8 = data orelse 0;

This can also be used to do things like early return or break a loop:

1
var definitely_data: u8 = data orelse return false;
2
// continue using definitely_data

It’s also possible to define optional values on structs using the ?:

1
const MyStruct = struct {
2
required: u8,
3
optional: ?u8,
4
};

Another shorthand is using .? to mean orelse unreachable:

1
const x: ?u8 = 5;
2
const y: u8 = x orelse unreachable;

Methods

Structs can have methods on them. If the first argument is an instance of the struct of a pointer to one then it can be namespaced by the instance instead of being namespaced by the struct

1
const MyStruct = struct {
2
required: u8,
3
optional: ?u8,
4
5
// this can be any name you want, self is fine:
6
pub fn bigRequired(self: *MyStruct) u8 {
7
return self.required * 2;
8
}
9
};

Non-Values

Zig has different indicators for non-values:

  1. undefined - not yet a value
  2. null - explicitly non-value
  3. errors - no value because there was an error
  4. void - type stating there will never be a value

References