Output & Running
Hello, World
console.log("Hello, World!"); const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
try stdout.writeStreamingAll(io, "Hello, World!\n");
} Zig has no
console.log and no runtime to provide one — you import std, get a handle to standard output, and write bytes to it. The !void return type means main may fail with an error, and try forwards any write error. Zig adds no trailing newline, so \n is explicit.Formatted output
const name = "Alice";
const age = 30;
console.log(`${name} is ${age}`); const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [64]u8 = undefined;
const name = "Alice";
const age: u32 = 30;
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{s} is {d}\n", .{ name, age }));
} Zig formats into a caller-provided buffer with
std.fmt.bufPrint — no allocation, and the format placeholders ({s} for strings, {d} for integers) are checked against their argument types at compile time, unlike JavaScript template literals.Compiling & running
// Run a script directly:
// node app.js
//
// JavaScript is interpreted/JIT-compiled at startup — no build step. // One-shot compile and run:
// zig run app.zig
//
// Build a native binary:
// zig build-exe app.zig
// ./app
//
// Release build (optimized):
// zig build-exe -O ReleaseFast app.zig Where Node interprets your source at startup, Zig compiles ahead of time to a standalone native binary with no runtime or interpreter.
zig run compiles and executes in one step for convenience, much like running a script.Variables & Types
const & var
let count = 3;
const limit = 100;
count = count + 1;
console.log(count, limit); const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [32]u8 = undefined;
var count: i32 = 3;
const limit: i32 = 100;
count = count + 1;
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d} {d}\n", .{ count, limit }));
} Zig also has
const and var, but the type follows the name after a colon (count: i32). Unlike JavaScript, an unused local variable is a compile error, and a mutable var that is never reassigned warns — the compiler nudges you toward const.Static types vs. dynamic
let value = 42;
value = "now a string";
console.log(typeof value); const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [32]u8 = undefined;
const value = 42; // type inferred, fixed forever
// value = "text"; // compile error: cannot reassign const, wrong type anyway
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d}\n", .{value}));
} A JavaScript variable can hold any type and change at runtime. Zig is statically typed: every value has one fixed, compile-time-known type. The compiler infers it from the initializer, but it can never change.
Booleans
const ready = true;
const done = false;
console.log(ready && !done); const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [8]u8 = undefined;
const ready: bool = true;
const done: bool = false;
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{}\n", .{ready and !done}));
} Zig's boolean operators are the words
and, or, and !. There is no implicit conversion: 0, "", and null are not booleans, so a condition must be a real bool — none of JavaScript's truthy/falsy coercion.Numbers
Sized number types
// JavaScript has one number type (64-bit float)
const a = 255;
const b = -2147483648;
const c = 3.14;
console.log(a, b, c); const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [64]u8 = undefined;
const a: u8 = 255;
const b: i32 = -2147483648;
const c: f64 = 3.14;
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d} {d} {d:.2}\n", .{ a, b, c }));
} JavaScript has a single
number type (a 64-bit float). Zig has explicit sized integers (u8, i32, u64, …) and floats (f32, f64), so you choose the exact width and signedness — and the compiler enforces that values fit.Arithmetic & division
console.log(7 + 2);
console.log(7 / 2);
console.log(Math.floor(7 / 2));
console.log(7 % 2); const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [32]u8 = undefined;
const a: i32 = 7;
const b: i32 = 2;
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d} {d} {d}\n", .{ a + b, @divTrunc(a, b), @rem(a, b) }));
} Because Zig integers are not floats, integer division uses an explicit builtin:
@divTrunc (toward zero) or @divFloor, and @rem for remainder. There is no single / that silently switches between integer and floating-point division as JavaScript's does.Overflow is checked
// JavaScript floats lose precision but never "overflow" to garbage
let value = 255;
value = value + 1;
console.log(value); const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [16]u8 = undefined;
const value: u8 = 255;
// value + 1 would PANIC in a safe build (overflow). Opt into wrapping:
const wrapped = value +% 1;
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d}\n", .{wrapped}));
} Adding past a fixed integer's range is a checked error in Zig: in a safe build it panics rather than silently wrapping. When you actually want wraparound you ask for it explicitly with
+%. JavaScript numbers, being floats, instead lose precision.Strings
Strings are byte slices
const greeting = "hello";
console.log(greeting.length);
console.log(greeting[0]); const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [16]u8 = undefined;
const greeting: []const u8 = "hello";
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d} {c}\n", .{ greeting.len, greeting[0] }));
} A Zig string is not a distinct type — it is
[]const u8, a slice of bytes. Length is the .len field (not a .length property), and indexing yields a single byte (u8), printed with {c}, not a one-character string.Concatenation
const first = "foo";
const second = "bar";
console.log(first + second); const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
// ++ concatenates at compile time (lengths must be known then)
const combined = "foo" ++ "bar";
try stdout.writeStreamingAll(io, combined);
try stdout.writeStreamingAll(io, "\n");
} JavaScript joins strings at runtime with
+. Zig's ++ concatenates only compile-time-known arrays. Joining runtime strings requires an allocator (for example std.fmt.allocPrint) because the result's size is not known until run time — Zig never hides an allocation.Arrays & Slices
Fixed-size arrays
const numbers = [10, 20, 30];
console.log(numbers[0], numbers.length); const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [32]u8 = undefined;
const numbers = [_]i32{ 10, 20, 30 };
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d} {d}\n", .{ numbers[0], numbers.len }));
} A Zig array's length is part of its type:
[3]i32 and [4]i32 are different types. [_]i32{...} lets the compiler count the elements. Unlike a JavaScript array, the size is fixed at compile time and cannot grow.Slices
const numbers = [10, 20, 30, 40, 50];
const middle = numbers.slice(1, 4);
console.log(middle); const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [32]u8 = undefined;
const numbers = [_]i32{ 10, 20, 30, 40, 50 };
const middle = numbers[1..4]; // a slice: pointer + length, no copy
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{any}\n", .{middle}));
} Zig slicing with
numbers[1..4] produces a []const i32 — a view (pointer plus length) into the original array, with no copy. JavaScript's slice() always allocates a new array.Iterating
const colors = ["red", "green", "blue"];
for (const color of colors) {
console.log(color);
} const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
const colors = [_][]const u8{ "red", "green", "blue" };
for (colors) |color| {
try stdout.writeStreamingAll(io, color);
try stdout.writeStreamingAll(io, "\n");
}
} Zig's
for (colors) |color| captures each element in the |...| payload — the counterpart to JavaScript's for...of. To also get the index, write for (colors, 0..) |color, index|.Summing with an index
const numbers = [10, 20, 30, 40];
let total = 0;
numbers.forEach((value, index) => {
total += value;
});
console.log(total); const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [16]u8 = undefined;
const numbers = [_]i32{ 10, 20, 30, 40 };
var total: i32 = 0;
for (numbers) |value| {
total += value;
}
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d}\n", .{total}));
} Zig has no
forEach, map, or reduce methods on arrays — there are no methods on built-in types at all. You write an explicit loop, which compiles to exactly the machine code you would expect with no callback overhead.Control Flow
if as an expression
const score = 75;
const grade = score >= 60 ? "pass" : "fail";
console.log(grade); const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
const score = 75;
const grade = if (score >= 60) "pass" else "fail";
try stdout.writeStreamingAll(io, grade);
try stdout.writeStreamingAll(io, "\n");
} Zig has no ternary
?: operator; instead if itself is an expression that yields a value, so if (cond) a else b covers the same ground. Both branches must produce the same type.Exhaustive switch
const day = 3;
switch (day) {
case 1: console.log("Mon"); break;
case 2: console.log("Tue"); break;
case 3: console.log("Wed"); break;
default: console.log("other");
} const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
const day: u8 = 3;
const name = switch (day) {
1 => "Mon",
2 => "Tue",
3 => "Wed",
else => "other",
};
try stdout.writeStreamingAll(io, name);
try stdout.writeStreamingAll(io, "\n");
} Zig's
switch is an expression with no fall-through and no break — each prong uses =>. It must be exhaustive: every possible value needs a prong or an else, so forgetting a case is a compile error, not a silent bug as in JavaScript.Loops
while loop
let count = 3;
while (count > 0) {
console.log(count);
count -= 1;
} const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [8]u8 = undefined;
var count: i32 = 3;
while (count > 0) : (count -= 1) {
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d}\n", .{count}));
}
} Zig's
while can carry a "continue expression" in : (count -= 1) that runs after every iteration. Unlike putting the decrement at the end of the body, it still runs when you continue, avoiding a classic infinite-loop bug.Counting loop
for (let index = 0; index < 3; index++) {
console.log(index);
} const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [8]u8 = undefined;
for (0..3) |index| {
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d}\n", .{index}));
}
} Zig has no C-style three-part
for. To count, you iterate a range with for (0..3) |index|, which yields 0, 1, 2 (the upper bound is exclusive, like JavaScript array indices).Functions
Defining functions
function add(first, second) {
return first + second;
}
console.log(add(3, 4)); const std = @import("std");
fn add(first: i32, second: i32) i32 {
return first + second;
}
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [8]u8 = undefined;
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d}\n", .{add(3, 4)}));
} Zig functions are declared with
fn and require a type on every parameter and on the return value (i32 here). There is no implicit coercion of arguments — passing a float where an i32 is expected is a compile error.Returning multiple values
function minMax(numbers) {
return [Math.min(...numbers), Math.max(...numbers)];
}
const [low, high] = minMax([3, 1, 4, 1, 5]);
console.log(low, high); const std = @import("std");
fn minMax(numbers: []const i32) struct { min: i32, max: i32 } {
var low = numbers[0];
var high = numbers[0];
for (numbers) |value| {
if (value < low) low = value;
if (value > high) high = value;
}
return .{ .min = low, .max = high };
}
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [32]u8 = undefined;
const result = minMax(&[_]i32{ 3, 1, 4, 1, 5 });
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d} {d}\n", .{ result.min, result.max }));
} Zig has no tuple-destructuring return like JavaScript's array. Instead a function returns an anonymous
struct with named fields (.{ .min = ..., .max = ... }), and the caller reads result.min — clearer, and with no heap allocation.Optionals
null & undefined vs. optionals
function find(numbers, target) {
const index = numbers.indexOf(target);
return index === -1 ? null : index;
}
const result = find([10, 20, 30], 20);
if (result !== null) {
console.log("found at", result);
} const std = @import("std");
fn find(numbers: []const i32, target: i32) ?usize {
for (numbers, 0..) |value, index| {
if (value == target) return index;
}
return null;
}
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [32]u8 = undefined;
const numbers = [_]i32{ 10, 20, 30 };
if (find(&numbers, 20)) |index| {
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "found at {d}\n", .{index}));
}
} Zig encodes "maybe absent" in the type with a leading
? (?usize). You cannot use the value without unwrapping it, and if (opt) |value| binds the guaranteed-present value inside the block — eliminating the "undefined is not a function" class of bugs at compile time.Default values
function getPort(configured) {
return configured ?? 8080;
}
console.log(getPort(null));
console.log(getPort(3000)); const std = @import("std");
fn getPort(configured: ?u16) u16 {
return configured orelse 8080;
}
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [16]u8 = undefined;
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d}\n", .{getPort(null)}));
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d}\n", .{getPort(3000)}));
} Zig's
orelse supplies a fallback when an optional is null — the direct counterpart to JavaScript's nullish-coalescing ??. Its right-hand side can also return or break for concise early exits.Error Handling
Errors are values, not exceptions
function openPort(port) {
if (port < 1024) {
throw new Error("PermissionDenied");
}
return port;
}
try {
openPort(80);
} catch (error) {
console.log("error:", error.message);
} const std = @import("std");
const PortError = error{ PermissionDenied };
fn openPort(port: u16) PortError!u16 {
if (port < 1024) return PortError.PermissionDenied;
return port;
}
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [64]u8 = undefined;
if (openPort(80)) |port| {
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "opened {d}\n", .{port}));
} else |err| {
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "error: {s}\n", .{@errorName(err)}));
}
} Zig has no exceptions. A function that can fail returns an error union, written
PortError!u16 — either an error from the set or a valid value. The caller must handle both arms; you cannot accidentally ignore an error as an unchecked JavaScript throw lets you.try & catch
function parsePort(text) {
const value = Number(text);
if (Number.isNaN(value)) throw new Error("bad number");
return value;
}
// Propagate, or recover with a default:
let port;
try {
port = parsePort("abc");
} catch {
port = 8080;
}
console.log(port); const std = @import("std");
fn parsePort(text: []const u8) !u16 {
return std.fmt.parseInt(u16, text, 10);
}
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [16]u8 = undefined;
// catch supplies a fallback; try would propagate the error instead
const port = parsePort("abc") catch 8080;
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d}\n", .{port}));
} Two keywords split JavaScript's single
try/catch: Zig's try expr propagates an error to the caller, while expr catch fallback recovers with a default value right there. Both make error handling explicit at each call site.Structs & Methods
Objects vs. structs
const point = { x: 3, y: 4 };
console.log(point.x, point.y); const std = @import("std");
const Point = struct {
x: f64,
y: f64,
};
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [32]u8 = undefined;
const point = Point{ .x = 3, .y = 4 };
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d:.0} {d:.0}\n", .{ point.x, point.y }));
} A Zig
struct declares its fields and their types up front, unlike a JavaScript object literal whose shape is open and dynamic. Field initializers are always named (.x = 3), and you cannot add fields a struct did not declare.Methods
class Circle {
constructor(radius) {
this.radius = radius;
}
area() {
return 3.14159 * this.radius ** 2;
}
}
console.log(new Circle(2).area().toFixed(2)); const std = @import("std");
const Circle = struct {
radius: f64,
fn area(self: Circle) f64 {
return 3.14159 * self.radius * self.radius;
}
};
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [16]u8 = undefined;
const circle = Circle{ .radius = 2 };
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d:.2}\n", .{circle.area()}));
} Methods are functions defined inside the
struct whose first parameter is self — there is no hidden this. Called as circle.area(), Zig passes the receiver as self explicitly. Structs do not inherit; Zig favors composition over class hierarchies.Enums & Tagged Unions
Enums
// JavaScript usually fakes enums with string constants
const Color = { Red: "red", Green: "green", Blue: "blue" };
const chosen = Color.Green;
console.log(chosen); const std = @import("std");
const Color = enum { red, green, blue };
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
const chosen = Color.green;
const name = switch (chosen) {
.red => "red",
.green => "green",
.blue => "blue",
};
try stdout.writeStreamingAll(io, name);
try stdout.writeStreamingAll(io, "\n");
} Zig has a real
enum type, where JavaScript improvises with an object of string constants. Switching on an enum is checked for exhaustiveness, so adding a new color and forgetting to handle it becomes a compile error.Tagged unions
// A "discriminated union" in JS: an object with a tag field
function area(shape) {
switch (shape.kind) {
case "circle": return 3.14159 * shape.radius ** 2;
case "square": return shape.side ** 2;
}
}
console.log(area({ kind: "circle", radius: 2 }).toFixed(2)); const std = @import("std");
const Shape = union(enum) {
circle: f64,
square: f64,
};
fn area(shape: Shape) f64 {
return switch (shape) {
.circle => |radius| 3.14159 * radius * radius,
.square => |side| side * side,
};
}
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [16]u8 = undefined;
const shape = Shape{ .circle = 2 };
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d:.2}\n", .{area(shape)}));
} A Zig
union(enum) is a type-safe tagged union: the value is exactly one variant, each carrying its own typed payload. Switching on it unwraps the payload in |...|, all checked at compile time — far safer than a hand-tagged JavaScript object.Memory & Allocators
Explicit allocation
// JavaScript allocates and garbage-collects automatically
function duplicate(source) {
return source.slice();
}
const copy = duplicate([1, 2, 3]);
console.log(copy); const std = @import("std");
fn duplicate(allocator: std.mem.Allocator, source: []const u8) ![]u8 {
const copy = try allocator.alloc(u8, source.len);
@memcpy(copy, source);
return copy;
}
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var debug_alloc: std.heap.DebugAllocator(.{}) = .init;
defer _ = debug_alloc.deinit();
const allocator = debug_alloc.allocator();
const copy = try duplicate(allocator, "hello");
defer allocator.free(copy);
try stdout.writeStreamingAll(io, copy);
try stdout.writeStreamingAll(io, "\n");
} Zig has no garbage collector. Any function that allocates takes an
allocator argument, making the cost visible in the signature, and the caller frees the memory. This is the central difference from JavaScript, where allocation and reclamation are invisible.defer vs. finally
function process() {
try {
console.log("working");
} finally {
console.log("cleanup");
}
}
process(); const std = @import("std");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
defer stdout.writeStreamingAll(io, "cleanup\n") catch {};
try stdout.writeStreamingAll(io, "working\n");
} Zig's
defer schedules a statement to run when the current scope exits, no matter how. It sits right next to the resource it cleans up — instead of a separate finally block far below — which keeps acquisition and release visually paired.Comptime & Generics
Compile-time execution
// JavaScript computes everything at runtime
function fibonacci(n) {
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}
console.log(fibonacci(10)); const std = @import("std");
fn fibonacci(n: u32) u32 {
return if (n < 2) n else fibonacci(n - 1) + fibonacci(n - 2);
}
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [16]u8 = undefined;
const result = comptime fibonacci(10); // computed during compilation
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d}\n", .{result}));
} The
comptime keyword runs ordinary Zig code during compilation, baking the result into the binary as a constant — there is no JavaScript equivalent. The same function works at run time or compile time without being written twice.Generics via comptime types
// JavaScript functions are generic for free (dynamic typing)
function maxOf(first, second) {
return first > second ? first : second;
}
console.log(maxOf(3, 7));
console.log(maxOf(2.5, 1.5)); const std = @import("std");
fn maxOf(comptime T: type, first: T, second: T) T {
return if (first > second) first else second;
}
pub fn main(init: std.process.Init) !void {
const io = init.io;
const stdout = std.Io.File.stdout();
var buf: [32]u8 = undefined;
try stdout.writeStreamingAll(io, try std.fmt.bufPrint(&buf, "{d} {d:.1}\n", .{ maxOf(i32, 3, 7), maxOf(f64, 2.5, 1.5) }));
} Where JavaScript functions are generic for free because of dynamic typing, Zig achieves generics by taking a
comptime T: type parameter — a type passed as an argument. The compiler generates a specialized, fully type-checked version for each type used.