Skip to content

Appendix H. Zig 0.16 Migration Notes

This appendix records the kinds of changes that matter when moving older Zig code to Zig 0.16. It is a practical checklist, not a full release history.

This appendix records the kinds of changes that matter when moving older Zig code to Zig 0.16. It is a practical checklist, not a full release history.

H.1 Start With the Compiler

Run the new compiler first.

zig version
zig build
zig test src/main.zig

Fix errors in this order:

  1. build script errors
  2. import and module errors
  3. standard library API errors
  4. type and builtin errors
  5. runtime behavior changes

Do not rewrite the program before reading the first compiler error.

H.2 Build Script Changes

Old build scripts usually fail before source files compile.

Check:

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
}

Artifacts should receive .target and .optimize.

const exe = b.addExecutable(.{
    .name = "app",
    .root_source_file = b.path("src/main.zig"),
    .target = target,
    .optimize = optimize,
});

Use b.path(...) for project-relative paths.

.root_source_file = b.path("src/main.zig")

H.3 Modules and Imports

Prefer explicit modules.

const lib_mod = b.createModule(.{
    .root_source_file = b.path("src/lib.zig"),
    .target = target,
    .optimize = optimize,
});

exe.root_module.addImport("lib", lib_mod);

Then import by name:

const lib = @import("lib");

This is cleaner than relying on path-shaped imports throughout the program.

H.4 Package Dependencies

Use build.zig.zon for package metadata and dependencies.

Typical project files:

build.zig
build.zig.zon
src/main.zig

A dependency is read in build.zig.

const dep = b.dependency("name", .{
    .target = target,
    .optimize = optimize,
});

exe.root_module.addImport("name", dep.module("name"));

Source files import the module.

const name = @import("name");

H.5 Standard Library Movement

The standard library changes between Zig versions. Treat std as source code you can inspect.

Search by name:

grep -R "pub fn openFile" "$(zig env | jq -r .std_dir)"

Or open the standard library directory:

zig env

Common areas that may need edits:

std.fs
std.io
std.mem
std.process
std.Build
std.http
std.json

When an API changed, do not guess the replacement from memory. Read the new declaration.

H.6 I/O Code

I/O APIs are common migration points.

Old code often has one of these shapes:

const stdout = std.io.getStdOut().writer();
try stdout.print("hello\n", .{});

or file reading code like:

const data = try file.readToEndAlloc(allocator, max_size);

Check Zig 0.16 declarations directly if these fail. I/O is an area where names and interfaces may move during standard library work.

H.7 Memory Utilities

Check old memory calls.

std.mem.copy
std.mem.set

Modern code commonly uses more explicit names or builtins.

std.mem.copyForwards
std.mem.copyBackwards
@memcpy
@memset

Use copyForwards or copyBackwards when overlap direction matters. Use @memcpy when the source and destination must not overlap.

H.8 Integer Casts

Older code may use casts that no longer match the current builtin signatures.

Prefer destination type from context:

const x: u32 = @intCast(n);

Use @as when the result type is not clear from context.

const x = @as(u32, @intCast(n));

Do the same for floats and enum conversion where needed.

H.9 Pointer Casts

Pointer casts should be explicit about intent.

const p: *T = @ptrCast(raw);

If alignment increases, add @alignCast.

const p: *T = @alignCast(@ptrCast(raw));

If the conversion changes constness, fix the type instead of forcing the cast.

H.10 Error Handling

Old code may ignore errors.

file.writeAll(data);

Zig rejects this.

try file.writeAll(data);

Or handle the error locally.

file.writeAll(data) catch |err| {
    return err;
};

Do not silence errors with catch {} unless losing the error is correct.

H.11 Optional Handling

Check old null-handling code.

const value = maybe.?;

A force unwrap is acceptable only when null means a programmer bug.

Safer forms:

const value = maybe orelse return error.MissingValue;
if (maybe) |value| {
    use(value);
}

H.12 comptime Code

Compile-time code may break because of stricter type checking or changed reflection shapes.

Common places:

@typeInfo
@Type
@hasDecl
@hasField
@field
inline for

Keep compile-time code small. Print intermediate values with:

@compileLog(x);

Remove @compileLog after the migration.

H.13 Tests

Run all tests after each group of edits.

zig build test
zig test src/main.zig

Use std.testing.allocator in tests that allocate.

const allocator = std.testing.allocator;

Free what you allocate.

const buf = try allocator.alloc(u8, 1024);
defer allocator.free(buf);

H.14 C Interop

Check C interop in three places:

@cImport blocks
extern declarations
build.zig linking

Build script:

exe.linkLibC();
exe.addIncludePath(b.path("include"));
exe.linkSystemLibrary("sqlite3");

C declaration:

extern fn puts(s: [*:0]const u8) c_int;

When translated headers fail, reduce the header surface. Import the smallest C header that gives the declarations you need.

H.15 Release Modes

Check behavior in all modes that the project supports.

zig build -Doptimize=Debug
zig build -Doptimize=ReleaseSafe
zig build -Doptimize=ReleaseFast
zig build -Doptimize=ReleaseSmall

Debug and ReleaseSafe keep more safety checks. ReleaseFast and ReleaseSmall remove many checks.

Do not rely on a safety trap as part of normal program behavior.

H.16 Migration Checklist

Use this order:

1. install Zig 0.16
2. run zig version
3. run zig build
4. fix build.zig
5. fix modules and imports
6. fix standard library API calls
7. fix builtin calls and casts
8. fix error handling
9. run tests
10. run release builds

A good migration leaves the code simpler than before. Delete compatibility wrappers once all code uses the new form.