Open-Source Wikis

/

Bun

/

How to contribute

/

Patterns and conventions

oven-sh/bun

Patterns and conventions

The non-negotiable style rules. The authoritative references are CLAUDE.md (root), src/CLAUDE.md (Zig), src/js/CLAUDE.md (TS builtins), and test/CLAUDE.md (tests). This page summarises and links them.

Zig

Use bun.* not std.*

bun.sys, bun.path, bun.strings, bun.spawn, bun.default_allocator, bun.PathBuffer are wrappers that work cross-platform and produce useful errors. std.fs, std.posix, std.os may panic on Windows or discard error context. Always prefer bun.*.

// ✓
const fd = switch (bun.sys.open(path, bun.O.RDONLY, 0)) {
    .result => |fd| fd,
    .err => |err| return .{ .err = err },
};

// ✗ — discards path on error, panics on Windows
const fd = try std.fs.cwd().openFile(path, .{}).handle;

See Allocators & sys.

Use Maybe(T) consistently

Every bun.sys.* call returns Maybe(T). Always switch on it. Don't unreachable on unexpected errnos — propagate them.

Use bun.handleOom, not catch unreachable

// ✓ — converts OOM to a stable crash, propagates other errors
const data = bun.handleOom(allocator.alloc(u8, n));

// ✗ — swallows non-OOM errors
const data = allocator.alloc(u8, n) catch bun.outOfMemory();

Path scratch space

Use bun.PathBuffer (a 4 KB stack buffer). For deep call stacks where 4 KB on the stack is too much (Windows!), use bun.path_buffer_pool to acquire / release.

Imports at the bottom

Top-level const imports go at the bottom of the file. The auto-formatter moves them. Don't fight it.

Never @import() inside functions

Top-of-file or bottom-of-file only. Inline imports break the formatter and tooling.

Logging

const log = bun.Output.scoped(.MyScope, .visible);
log("loading {s}", .{path});

Set .visible for production-relevant messages, .hidden for noisy diagnostics. .hidden only fires when BUN_DEBUG_<scope>=1.

For user-facing output, use bun.Output.pretty("<green>ok<r>: {s}\n", .{path}). Tag colours work cross-platform via the global Output.enable_ansi_colors flag.

Exception scope (JSC bindings)

Any Zig function that calls into JSC must hold a JSC.ExceptionScope and re-check after the call. The pattern is shown in nearly every file under src/bun.js/api/.

const args = callframe.arguments();
const result = JSC.JSValue.fromCell(...).toString(globalObject) catch return .zero;
if (globalObject.hasException()) return .zero;

Run with BUN_JSC_validateExceptionChecks=1 to catch missing checks.

TypeScript builtins (src/js/)

See Built-in JS modules and src/js/CLAUDE.md for the full reference. Highlights:

  • Use .$call/.$apply, never .call/.apply. User-mutable global prototypes can break un-prefixed call.
  • require() with a string literal only. Dynamic specifiers don't compile.
  • export default { ... }. The bundler converts to return.
  • $intrinsic markers are real — $Array.from, $ArrayPrototype.map.$call, $putByIdDirectPrivate, etc.
  • process.platform and process.arch are dead-code eliminated at build time.
  • Run bun bd after changes to regenerate.

C++ JSC bindings

See C++ bindings. Patterns:

  • Three classes for a public-constructor type: Foo (JSDestructibleObject), FooPrototype, FooConstructor.
  • HashTableValue arrays for static properties.
  • Iso subspace + cached structure in ZigGlobalObject.
  • Match generated style. When in doubt, look at what generate-classes.ts produces and match it.

Naming and layout

  • Files are named by the type they primarily define: Watcher.zig defines Watcher. bundle_v2.zig defines BundleV2.
  • Directory structure mirrors logical structure (src/bun.js/api/server.zig is the JS-visible server API).
  • *.classes.ts lives next to its .zig implementation.

Cross-platform

  • Always test paths on Windows. bun.path.windows vs bun.path.posix.
  • Always use bun.sys.* rather than POSIX-only equivalents.
  • For platform-specific code, use comptime checks: if (comptime Environment.isWindows) { ... }. Environment is in src/env.zig.

"Why this and not the alternative?"

The repo asks contributors to write a one-line answer to that question for any non-obvious choice. If you can't, research first. Don't write code in the hope that the rationale will appear later.

"If neighbouring code is different, find out why first"

Existing patterns in Bun are usually load-bearing — they're there because someone discovered an edge case. Before deviating, grep for similar code and read the surrounding history.

Honesty

NEVER overstate what you got done or what actually works in commits, PRs, or in messages to the user. — CLAUDE.md

If a PR says "fixed and tested", reviewers expect the test to fail on main and pass on the branch. Anything less is misleading.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Patterns and conventions – Bun wiki | Factory