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 toreturn.$intrinsicmarkers are real —$Array.from,$ArrayPrototype.map.$call,$putByIdDirectPrivate, etc.process.platformandprocess.archare dead-code eliminated at build time.- Run
bun bdafter 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.tsproduces and match it.
Naming and layout
- Files are named by the type they primarily define:
Watcher.zigdefinesWatcher.bundle_v2.zigdefinesBundleV2. - Directory structure mirrors logical structure (
src/bun.js/api/server.zigis the JS-visible server API). *.classes.tslives next to its.zigimplementation.
Cross-platform
- Always test paths on Windows.
bun.path.windowsvsbun.path.posix. - Always use
bun.sys.*rather than POSIX-only equivalents. - For platform-specific code, use
comptimechecks:if (comptime Environment.isWindows) { ... }.Environmentis insrc/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.