# Idiomatic Zig and Common Pitfalls — Zig

Source: https://www.geekswithgeeks.com/en/zig/p-idioms

> Write clear Zig and avoid frequent mistakes.

## Writing Zig the Zig way

Idiomatic Zig is **explicit and boring** in the best sense. Pass **allocators** to anything that allocates, and document ownership ("caller owns the returned memory"). Pair every acquisition with **`defer`** or **`errdefer`** on the next line. Return **error unions** with meaningful error sets, use **`try`** to propagate, and handle errors where you can do something useful. Prefer **slices** to many-item pointers, **tagged unions** and exhaustive `switch` for alternatives, **`const`** wherever possible, and **`comptime`** for validation and specialisation rather than clever metaprogramming. Run `zig fmt` and follow the naming conventions: `camelCase` functions, `TitleCase` types, `snake_case` variables and fields. Common pitfalls: **dangling pointers** to stack variables or into a reallocated `ArrayList`; storing **non-owned string keys** in hash maps; forgetting to **free** (caught by `std.testing.allocator`) or freeing with the **wrong allocator**; **forgetting `flush`** on buffered writers; relying on undefined behaviour that only appears in ReleaseFast; **`catch unreachable`** on errors that can actually happen; using `undefined` values before setting them; and copying examples written for **older Zig versions**, whose std APIs may no longer exist.

## Pitfalls and their fixes

Dangling pointers, catch unreachable and hash map keys.

```zig
const std = @import("std");

// PITFALL: returns a slice of a stack buffer that dies when the function returns
fn badGreeting(name: []const u8) []const u8 {
    var buf: [64]u8 = undefined;
    return std.fmt.bufPrint(&buf, "Hi {s}", .{name}) catch "Hi";   // dangling!
}

// FIX: let the caller provide the buffer (or pass an allocator)
fn greeting(buf: []u8, name: []const u8) ![]const u8 {
    return std.fmt.bufPrint(buf, "Hi {s}", .{name});
}

// PITFALL: catch unreachable on input that can be invalid
fn parsePortBad(text: []const u8) u16 {
    return std.fmt.parseInt(u16, text, 10) catch unreachable;   // panics (or UB in ReleaseFast)
}

// FIX: return the error, or handle it with a meaningful default
fn parsePort(text: []const u8) !u16 {
    return std.fmt.parseInt(u16, text, 10);
}

// PITFALL: hash map keys pointing at a buffer that is reused
fn countWords(gpa: std.mem.Allocator, words: []const []const u8) !std.StringHashMap(u32) {
    var map = std.StringHashMap(u32).init(gpa);
    errdefer map.deinit();
    for (words) |w| {
        const entry = try map.getOrPut(w);          // OK here: words outlive the map
        if (!entry.found_existing) entry.value_ptr.* = 0;
        entry.value_ptr.* += 1;
    }
    return map;   // if keys came from a reused read buffer, dupe them with gpa.dupe(u8, w)
}

test "fixed versions" {
    var buf: [32]u8 = undefined;
    try std.testing.expectEqualStrings("Hi Asha", try greeting(&buf, "Asha"));
    try std.testing.expectError(error.InvalidCharacter, parsePort("80a"));

    var map = try countWords(std.testing.allocator, &.{ "pen", "ink", "pen" });
    defer map.deinit();
    try std.testing.expectEqual(@as(?u32, 2), map.get("pen"));
}
```

## unreachable is a promise

Writing `unreachable` tells the compiler a path can never happen. In safe modes it panics if you were wrong; in ReleaseFast it is undefined behaviour. Use it only for real invariants, never for input you do not control.

**Quiz:** Why is returning a slice of a local array from a function a bug?

- [x] The array lives on the function's stack frame, so the slice dangles after return
- [ ] Slices cannot be returned
- [ ] It is slow
- [ ] Arrays are always heap-allocated

*Answer:* The array lives on the function's stack frame, so the slice dangles after return. Stack memory is reclaimed when the function returns.
