Zig: The Good Parts (1)

Learning Zig as a front-end developer involves plenty of unlearning. JavaScript is lenient and even TypeScript allows for mischief. With Zig, and basically everything that isn’t JavaScript, the rules of engagement are strict. Zig is a low-level compiled programming language. […]


This content originally appeared on dbushell.com and was authored by dbushell.com

Learning Zig as a front-end developer involves plenty of unlearning. JavaScript is lenient and even TypeScript allows for mischief. With Zig, and basically everything that isn’t JavaScript, the rules of engagement are strict.

Zig logo

Zig is a low-level compiled programming language. That means unlike JavaScript it doesn’t come with a garbage collector to bury the bodies. Zig is considered a modern C and alternative to Rust. I’ve tried Rust many times and I just bounce off it. Rust makes me feel stupid. I tried Zig and it stuck. Zig made me realise I wasn’t so stupid.

The Good Parts

In this blog post I’ve documented several Zig language features that I’ve learnt and liked so far (and a few I don’t):

There’s plenty more good parts to Zig from the General Purpose Allocator to the Zig Standard Library. As I continue learning Zig I’ll be blogging more of my discoveries.

I started learning Zig because I got bored of solving Advent of Code puzzles with TypeScript. Zig can compile to WASM so there could be practical applications in my day job. Although I’m unlikely to need any WASM for the websites I build. Maybe one day?

Disclaimer: I started learn Zig this year. It’s still January. These are initial observations from a front-end developer, i.e. two decades of hammering stuff with JavaScript. Don’t read this and go crying on Hacker News if I’m wrong, you knew what you were getting into.

Errors are useful

I know what you’re thinking, who enjoys handling errors? That’s because JavaScript error exceptions are a major chore. Due to tediousness, many JS devs avoid throwing errors and instead return nullish or falsy values to deal with.

In Zig, errors are first class citizens. In practice, Zig errors work like a union type that is somewhere between enums and optionals (discussed later).

Error types are defined and returned like an enum:

const BankError = error{Denied, Overdrawn};

fn getBalance() !i32 {
  return BankError.Denied;
}

The exclamation ! return type prefix denotes a possible error. The return type can be more specific with an error set:

fn getBalance() BankError!i32 {
  return BankError.Denied;
}

There is a shorthand for single errors without defining a set:

fn getBalance() !i32 {
  return error.Denied;
}

Error handling is where Zig gets fun because you can ignore errors in multiple convenient ways. The try keyword either unwraps the value or yeets the error back up the stack.

pub fn main() !void {
  const balance: i32 = try getBalance();
}

Here the main entry function must have the return type !void because using try is effectively syntactic sugar for:

pub fn main() !void {
  const balance: i32 = getBalance() catch |err| return err;
}

Here you can see the power of catch as part of the expression. catch can be used to provide a default value on error:

const balance: i32 = getBalance() catch 0;

The example above handles the error cleanly.

If you’re living life to the fullest you could “handle” an error with unreachable.

const balance: i32 = getBalance() catch unreachable;

If the unreachable keyword is ever reached Zig will panic. There are times you need to tell the compiler a code path is impossible. This is probably not the best way to handle errors. I suppose it’s better than using try and just ignoring the error until it bubble up to main() !void and exiting ungracefully.

A catch can even use a labelled block:

const balance: i32 = getBalance() catch |err| label: {
  if (err == BankError.Overdrawn) applyAdminFee();
  break :label 0;
};

This allows you to handle an error and return a new value in the same assignment.

Errors can also be captured and handled by conditional statements:

if (getBalance()) |balance| {
  print("Balance: {d}\n", .{balance});
} else |err| {
  print("Broke error: ({any})\n", .{err});
}

Zig also has an errdefer keyword that allows cleanup on error:

errdefer applyAdminFee();
const balance = try getBalance();

In the example above Zig return the parent function with the unhandled error, but not before running the errdefer code. If there is no error then errdefer never executes. More on defer later.

As you can see, Zig provides many ways to use errors for convenience. Compare this to JavaScript where exception handling is nothing but an inconvenience. JavaScript’s try catch syntax leads to awkward scope issues:

let balance = 0;
try {
  balance = getBalance();
} catch (err) {
  balance = 0;
}

JavaScript errors are practically invisible and TypeScript can’t help. JSDoc @throws is a small lifeline but most JavaScript developers avoid using errors because they hate handling them. It only get worse with async and promises. When your runtime has global events like unhandledrejection it’s a sign!

I’m led to believe other languages do errors similar to Zig. Coming from JavaScript it’s a welcome change.

Optionals are handy

Optionals are a special type that can be null or a specific typed value. In the example below the getThing function returns an optional Thing.

const maybe_thing: ?Thing = getThing();

if conditions can capture and unwrap an optional:

if (maybe_thing) |thing| {
  // thing is a constant in this scope...
}

They can also check for null:

if (maybe_thing == null) {
  // Do something else...
}

It’s also possible to unwrap with maybe_thing.? but if the optional is null an error is returned. The orelse keyword is used to assign defaults if the expression before it evaluates to null:

const thing: i32 = getThing() orelse default_thing;

Optional types are prefixed with ? like this function return type:

fn getThing() ?Thing {
  // Return `Thing` or `null`...
}

If a function returns an optional or an error type it will be prefixed like: !?Thing.

Zig has type inference for assignments so my previous examples could skip the type:

const maybe_thing = getThing();
const thing = getThing() orelse default_thing;

I find explicitly defining the type makes code more readable. Return types for functions are mandatory and not inferred. Optionals are great for function parameters. Zig does not have default parameters, instead you can pass and check for null.

Loops are concise

Zig for loops can iterate multiple things at once including ranges:

const string = "Hello, World!";
for (string, 0..) |character, index| {
  // Do stuff...
}

while loops have a ‘continue expression’ that is executed before the next iteration like the 3rd part of a JavaScript for. It’s useful to count iterations, and maybe more imaginative things?

var lines = std.mem.splitScalar(u8, input, '\n');
var count: usize = 0;
while (lines.next()) |line| : (count += 1) {
  // Do stuff...
}

I found the pipe | syntax weird but it quickly becomes second nature. At first I was flummoxed by the lack of three step let i = 0; i < Infinity; i++ loop syntax. Now when I go back to writing JavaScript my fingers ache typing it. It never felt cumbersome until I experience a difference way.

Learning Resources

That’s all for now! If I got stuff wrong @ me on social media.

The following links have been my source of knowledge:

Zig: The Bad Parts

I’m diggin’ Zig and it’s still early days so not much has started to annoy me. There are some minor things. The Zig compiler is incredibly strict about unused or unchanged variables.

main.zig:56:9: error: local variable is never mutated
    var will_change = 0;
        ^~~~~~~~~~~
main.zig:56:9: note: consider using 'const'

This can be very annoying when running code that is a work in progress. My variable will be mutated I just haven’t gotten that far. I would prefer this to be a warning, not an error, but the decision was made:

The Zig language is opinionated. There is one canonical way to do things. There will be no sloppy mode, and no divergence of compile errors from debug to release.

I can respect that, but whether or not I agree will remain in flux until I’m more experienced with Zig.

I have another minor gripe with imports. Zig allows me to import a local file:

const Thing = @import("./thing.zig");

But it does not allow:

const Thing = @import("../thing.zig");

At least not beyond the current working directory. Come on! I’m just trying to have fun reusing code. I don’t want to learn the module and build system just yet. I feel like this is unnecessary friction for learning with zig run main.zig.

Defer is simple

The defer keyword in Zig is very difference from “defer” in JavaScript. It executes the statement just before the block scope ends. Defer is often used to deallocate memory.

var things = ArrayList(Thing).init(allocator);
defer things.deinit();

Defer is handy for grouping code in a more organised and readable way. I like this pattern:

var previous: ?Thing = null;
for (things) |item| {
  defer previous = item;
  // Do a lot of stuff
}

I’m a big fan of defer it’s simple but powerful.


This content originally appeared on dbushell.com and was authored by dbushell.com


Print Share Comment Cite Upload Translate Updates
APA

dbushell.com | Sciencx (2025-01-27T10:00:00+00:00) Zig: The Good Parts (1). Retrieved from https://www.scien.cx/2025/01/27/zig-the-good-parts-1/

MLA
" » Zig: The Good Parts (1)." dbushell.com | Sciencx - Monday January 27, 2025, https://www.scien.cx/2025/01/27/zig-the-good-parts-1/
HARVARD
dbushell.com | Sciencx Monday January 27, 2025 » Zig: The Good Parts (1)., viewed ,<https://www.scien.cx/2025/01/27/zig-the-good-parts-1/>
VANCOUVER
dbushell.com | Sciencx - » Zig: The Good Parts (1). [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/01/27/zig-the-good-parts-1/
CHICAGO
" » Zig: The Good Parts (1)." dbushell.com | Sciencx - Accessed . https://www.scien.cx/2025/01/27/zig-the-good-parts-1/
IEEE
" » Zig: The Good Parts (1)." dbushell.com | Sciencx [Online]. Available: https://www.scien.cx/2025/01/27/zig-the-good-parts-1/. [Accessed: ]
rf:citation
» Zig: The Good Parts (1) | dbushell.com | Sciencx | https://www.scien.cx/2025/01/27/zig-the-good-parts-1/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.