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:
1pub fn main() void {2 // do stuff3}Imports
Imports are done using the @import built-in function. The Zig standard library can be imported using the @import function as such:
1const 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:
1var a: u8 = 50;2const b: u8 = 50;3const c = -11;Arrays
Arrays have a fixed length which can either be declared or inferred. Declaring an array inline looks like this:
1var data: [3]u32 = [3]u32{ 1, 2, 3 };Inferring the type and the size can be done like so:
1var data = [_]u32{ 1, 2, 3 };Accessing a value from an array can be done using index notation:
1const x = data[1];2data[2] = x;Arrays can be concatenated using ++:
1const a = [_]u8{ 1, 2 };2const b = [_]u8{ 3, 4 };3
4const c = a ++ b;
++is acomptimeoperator and runs during compilation
Declaring an array without assigning any values can be done using undefined:
1var data: [3]u8 = undefined;Strings
Zig stores strings as an array of bytes. Strings are denoted using double quotes (")
1const str = "hello";2const h = str[0];The ++ operator can also be used to concatenate strings
1const result = "hello" ++ " " ++ "world";Multiline strings can also be defined. These use \\ at the start of each line with no surrounding quotes:
1const multiline =2 \\First line of string3 \\Second line of string4;If/Else
If/else statements in Zig work as you’d expect:
1const a = 10;2
3if (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:
1const a = 10;2const price = if (a > 5) "high" else "low";While Loops
The syntax for a while loop is also pretty normal looking:
1var a = 10;2while (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:
1var a = 5;2while (a < 10) : (a += 1) {3 std.debug.print("{}\n", .{a})4}Or:
1var a = 5;2while (a < 10) : (a += 1) {3 // this will not print but will run the continue expression4 if (a == 7) continue;5 std.debug.print("{}\n", .{a})6}Zig also has break statements that exit the loop immediately, for example:
1var a = 5;2while (a < 10) : (a += 1) {3 // this will exit the loop4 if (a == 7) break;5 std.debug.print("{}\n", .{a})6}For Loops
For loops iterate over arrays and have the following structure:
1const arr = [_]{ 1, 2, 3 };2
3for (arr) |item| {4 // do stuff with item5}Additionally, they also support the ability to use a provided indexer:
1const arr = [_]{ 1, 2, 3 };2
3for (arr, 0..) |item, i| {4 // do stuff with item and index5}Functions
Functions are defined using the fn keyword along with a return type:
1fn doThing() u8 {2 return 10;3}Functions can also take parameters:
1fn 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:
1const MyCustomError = error {2 MyErrorA,3 MyErrorB,4 MyErrorC,5};A function can return an error as a normal value like so:
1fn 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:
1const result = myError(x);2
3if (x == MyCustomError.MyErrorA) {4 // do stuff5}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 number2const myValue: MyCustomError!u8 = 5;We can define a function that returns an error union like:
1fn 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 error2const result: u8 = doThing(x) catch 0;catch also can capture the error and do something with it:
1const result: u8 = doThing(x) catch |err| {2 if (err == MyCustomError.MyErrorA) {3 return 10;4 }5
6 return 0;7}Zig also has try:
1const result = try doThing();Which is basically shorthand for the following:
1const result = doThing() catch |err| return err;Zig can also infer an error by leaving out the type of error and specifying the
!resultTypebut this isn’t recommended for any function other thanmainwhich can returnvoidor failfn main() !void
Defer
The defer keyword will run some code after a block exits:
1pub fn main() void {2 // this will get run last3 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:
1errdefer cleanup();2const result = try doThing();Switch
Switch statements must be exhaustive or have an else branch. This looks like so:
1pub 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:
1pub 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 22const 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:
1if (valOrErr) |val| {2 // do things with val3} else |err| {4 // do things with err5}This can also be combined with a switch to handle specific types of errors:
1if (valOrErr) |val| {2 // do things with val3} else |err| switch (err) {4 MyCustomError.MyErrorA => {5 // handle error6 },7 MyCustomError.MyErrorB => {8 // handle error9 },10}Enums
Enums are values that can be one of a specific list. Defining an enum looks like so:
1const MyEnum = enum {2 valA,3 valB,4 valC,5};Enums are just numbers and so we can assign actual values to them as well:
1const 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:
1const MyStruct = struct {2 enu: MyEnum,3 val: u8,4};Creating an instance of a struct can be done using the following syntax:
1const thing = MyStruct {2 .enu = MyEnum.ValA,3 .val = 10,4};Structs properties can also accessed and modified:
1var thing = MyStruct {2 .enu = MyEnum.ValA,3 .val = 10,4};5
6thing.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:
1var a: u8 = 1;2const a_ptr: *u8 = &a;3
4const b: u8 = a_ptr.*;You can make
constpointers tovarvalues but you cannot makevarpointers toconstvalues
Zig also lets you define constant pointers which can point at constants or variables:
1var a: u8 = 1;2const a_ptr: *const u8 = &a;3
4const b: u8 = 1;5const b_prt: *const u8 = &b;Pointers let us pass values by reference, for example updating the passed variable:
1fn 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 ?:
1var data: ?u8 = 10;Optional values can be defaulted using orelse:
1var definitely_data: u8 = data orelse 0;This can also be used to do things like early return or break a loop:
1var definitely_data: u8 = data orelse return false;2// continue using definitely_dataIt’s also possible to define optional values on structs using the ?:
1const MyStruct = struct {2 required: u8,3 optional: ?u8,4};Another shorthand is using .? to mean orelse unreachable:
1const x: ?u8 = 5;2const 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
1const 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:
undefined- not yet a valuenull- explicitly non-valueerrors- no value because there was an errorvoid- type stating there will never be a value