| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 12 | 17 | 0% |
| Commands | 0 | 5 | 3 | 0% |
| Section tags | 2 | 2 | 4 | 25% |
What each file covers
Sections
0 shared · 12 only in A · 17 only in B- − Testing style
- − Spawning processes
- − No timeouts
- − Use port 0 to get a random port
- − Creating temporary files
- − Strings
- − Test Organization
- − Nested/complex object equality
- − Common Imports from `harness`
- − Error Testing
- − Avoid dynamic import & require
- − Test Utilities
- + Rust
- + Prefer `bun_core` / `bun_sys` over `std`
- + `bun_sys` — System Calls (`src/sys/`)
- + Strings (`bun_core::String` and `bun_core::strings`)
- + Paths (`bun_paths`)
- + URL Parsing (`bun_jsc::URL`)
- + MIME Types (`bun_http_types::MimeType`)
- + Memory & Allocators
- + Environment Variables (`bun_core::env_var`)
- + Logging (`bun_core::output`)
- + Spawning Subprocesses
- + JSC Interop & FFI Safety
- + Pointer provenance at FFI boundaries
- + `Strong` / `Weak` JS handles
- + Refcount transfer on `to_js()` / `create()`
- + Cross-thread string hazards
- + Common Patterns
Commands
0 shared · 5 only in A · 3 only in B- − bun bd test <...test file>
- − bun bd <...cmd>
- − bun:test
- − bun bd <file>
- − bun bd test <file>
- + cargo build
- + cargo check -p <crate>
- + bun bd
Section tags
2 shared · 2 only in A · 4 only in B- − test
- − testing-strategy
- + setup
- + build
- + types
- + performance
- code-style
- do-not
Line diff
oven-sh/bun · test/CLAUDE.md
@@ −1 @@
1To run tests:
2
3```sh
4bun bd test <...test file>
5```
6
7To run a command with your debug build of Bun:
8
9```sh
10bun bd <...cmd>
11```
12
13Note that compiling Bun may take up to 2.5 minutes. It is slow!
14
15**CRITICAL**: Do not use `bun test` to run tests. It will not have your changes. `bun bd test <...test file>` is the correct command, which compiles your code automatically.
16
17## Testing style
18
19Use `bun:test` with files that end in `*.test.{ts,js,jsx,tsx,mjs,cjs}`. If it's a test/js/node/test/{parallel,sequential}/\*.js without a .test.extension, use `bun bd <file>` instead of `bun bd test <file>` since those expect exit code 0 and don't use bun's test runner.
20
21- **Do not write flaky tests**. Unless explicitly asked, **never wait for time to pass in tests**. Always wait for the condition to be met instead of waiting for an arbitrary amount of time. **Never use hardcoded port numbers**. Always use `port: 0` to get a random port.
22- **Prefer concurrent tests over sequential tests**: When multiple tests in the same file spawn processes or write files, make them concurrent with `test.concurrent` or `describe.concurrent` unless it's very difficult to make them concurrent.
23
24### Spawning processes
25
26#### Spawning Bun in tests
27
28When spawning Bun processes, use `bunExe` and `bunEnv` from `harness`. This ensures the same build of Bun is used to run the test and ensures debug logging is silenced.
29
30##### Use `-e` for single-file tests
31
32```ts
33import { bunEnv, bunExe, tempDir } from "harness";
34import { test, expect } from "bun:test";
35
36test("single-file test spawns a Bun process", async () => {
37 await using proc = Bun.spawn({
38 cmd: [bunExe(), "-e", "console.log('Hello, world!')"],
39 env: bunEnv,
40 });
41
42 const [stdout, stderr, exitCode] = await Promise.all([
43 proc.stdout.text(),
44 proc.stderr.text(),
45 proc.exited,
46 ]);
47
48 expect(stderr).toBe("");
49 expect(stdout).toBe("Hello, world!\n");
50 expect(exitCode).toBe(0);
51});
52```
53
54##### When multi-file tests are required:
55
56```ts
57import { bunEnv, bunExe, tempDir } from "harness";
58import { test, expect } from "bun:test";
59
60test("multi-file test spawns a Bun process", async () => {
61 // If a test MUST use multiple files:
62 using dir = tempDir("my-test-prefix", {
63 "my.fixture.ts": `
64 import { foo } from "./foo.ts";
65 foo();
66 `,
67 "foo.ts": `
68 export function foo() {
69 console.log("Hello, world!");
70 }
71 `,
72 });
73
74 await using proc = Bun.spawn({
75 cmd: [bunExe(), "my.fixture.ts"],
76 env: bunEnv,
77 cwd: String(dir),
78 });
79
80 const [stdout, stderr, exitCode] = await Promise.all([
81
82 // ReadableStream in Bun supports:
83 // - `await stream.text()`
84 // - `await stream.json()`
85 // - `await stream.bytes()`
86 // - `await stream.blob()`
87 proc.stdout.text(),
88 proc.stderr.text(),
89
90 proc.exitCode,
91 ]);
92
93 expect(stdout).toBe("Hello, world!");
94 expect(stderr).toBe("");
95 expect(exitCode).toBe(0);
96```
97
98When a test file spawns a Bun process, we like for that file to end in `*-fixture.ts`. This is a convention that helps us identify the file as a test fixture and not a test itself.
99
100Generally, `await using` or `using` is a good idea to ensure proper resource cleanup. This works in most Bun APIs like Bun.listen, Bun.connect, Bun.spawn, Bun.serve, etc.
101
102#### Async/await in tests
103
104Prefer async/await over callbacks.
105
106When callbacks must be used and it's just a single callback, use `Promise.withResolvers` to create a promise that can be resolved or rejected from a callback.
107
108```ts
109const ws = new WebSocket("ws://localhost:8080");
110const { promise, resolve, reject } = Promise.withResolvers<void>(); // Can specify any type here for resolution value
111ws.onopen = resolve;
112ws.onclose = reject;
113await promise;
114```
115
116If it's several callbacks, it's okay to use callbacks. We aren't a stickler for this.
117
118### No timeouts
119
120**CRITICAL**: Do not set a timeout on tests. Bun already has timeouts.
121
122### Use port 0 to get a random port
123
124Most APIs in Bun support `port: 0` to get a random port. Never hardcode ports. Avoid using your own random port number function.
125
126### Creating temporary files
127
128Use `tempDirWithFiles` to create a temporary directory with files.
129
130```ts
131import { tempDir } from "harness";
132import path from "node:path";
133
134test("creates a temporary directory with files", () => {
135 using dir = tempDir("my-test-prefix", {
136 "file.txt": "Hello, world!",
137 });
138
139 expect(await Bun.file(path.join(String(dir), "file.txt")).text()).toBe(
140 "Hello, world!",
141 );
142});
143```
144
145### Strings
146
147To create a repetitive string, use `Buffer.alloc(count, fill).toString()` instead of `"A".repeat(count)`. "".repeat is very slow in debug JavaScriptCore builds.
148
149### Test Organization
150
151- Use `describe` blocks for grouping related tests
152- **Add tests to the existing test file for the code you're changing** — do not create a new file. Tests are organized by module (e.g., `/test/js/bun/`, `/test/js/node/`, `/test/js/web/`).
153- `/test/regression/issue/${issueNumber}.test.ts` is **only** for bugs that have a GitHub issue number **and** are true regressions (worked in a previous release, then broke). An issue number alone does not qualify — if it was never correct, put the test in the module's existing test file instead.
154- Integration tests are in `/test/integration/`
155
156### Nested/complex object equality
157
158Prefer usage of `.toEqual` rather than many `.toBe` assertions for nested or complex objects.
159
160<example>
161
162BAD (try to avoid doing this):
163
164```ts
165expect(result).toHaveLength(3);
166expect(result[0].optional).toBe(null);
167expect(result[1].optional).toBe("middle-value"); // CRITICAL: middle item's value must be preserved
168expect(result[2].optional).toBe(null);
169```
170
171**GOOD (always prefer this):**
172
173```ts
174expect(result).toEqual([
175 { optional: null },
176 { optional: "middle-value" }, // CRITICAL: middle item's value must be preserved
177 { optional: null },
178]);
179```
180
181</example>
182
183### Common Imports from `harness`
184
185```ts
186import {
187 bunExe, // Path to Bun executable
188 bunEnv, // Environment variables for Bun
189 tempDirWithFiles, // Create temporary test directories with files
190 tmpdirSync, // Create empty temporary directory
191 isMacOS, // Platform checks
192 isWindows,
193 isPosix,
194 gcTick, // Trigger garbage collection
195 withoutAggressiveGC, // Disable aggressive GC for performance tests
196} from "harness";
197```
198
199### Error Testing
200
201Always check exit codes and test error scenarios:
202
203```ts
204test("handles errors", async () => {
205 await using proc = Bun.spawn({
206 cmd: [bunExe(), "run", "invalid.js"],
207 env: bunEnv,
208 });
209
210 const exitCode = await proc.exited;
211 expect(exitCode).not.toBe(0);
212
213 // For synchronous errors
214 expect(() => someFunction()).toThrow("Expected error message");
215});
216```
217
218### Avoid dynamic import & require
219
220**Only** use dynamic import or require when the test is specifically testing something relataed to dynamic import or require. Otherwise, **always use module-scope import statements**.
221
222**BAD, do not do this**:
223
224```ts
225test("foo", async () => {
226 // BAD: Unnecessary usage of dynamic import.
227 const { readFile } = await import("node:fs");
228
229 expect(await readFile("ok.txt")).toBe("");
230});
231```
232
233**GOOD, do this:**
234
235```ts
236import { readFile } from "node:fs";
237test("foo", async () => {
238 expect(await readFile("ok.txt")).toBe("");
239});
240```
241
242### Test Utilities
243
244- Use `describe.each()` for parameterized tests
245- Use `toMatchSnapshot()` for snapshot testing
246- Use `beforeAll()`, `afterEach()`, `beforeEach()` for setup/teardown
247- Track resources (servers, clients) in arrays for cleanup in `afterEach()`
248
oven-sh/bun · src/CLAUDE.md
@@ +1 @@
1## Rust
2
3`src/` is a Cargo workspace (rooted at the repo's top-level `Cargo.toml`, ~200
4member crates). The runtime is built as `libbun_rust.a` via `cargo build -p
5bun_bin` (driven by `scripts/build/rust.ts`). Key crates:
6
7- `bun_core` (`src/bun_core/`) — strings, formatting, logging, env vars, allocator/heap helpers, the foundation everything else uses
8- `bun_sys` (`src/sys/`) — cross-platform syscall wrappers (`File`, `Fd`, `Dir`, `Error`)
9- `bun_paths` (`src/paths/`) — path joining/normalization, the path-buffer pool
10- `bun_jsc` (`src/jsc/`) — JSC value types, `Strong`/`Weak`, FFI imports, `URL`
11- `bun_runtime` (`src/runtime/`) — JS-visible APIs (server, fetch, node compat, crypto)
12- `bun_js_parser`, `bun_js_printer`, `bun_resolver`, `bun_bundler`, `bun_install`, `bun_collections`, `bun_threading`, `bun_alloc` — the rest of the pipeline
13- `bun_bin` (`src/bun_bin/`) — the staticlib root that `cargo build` links
14
15Conventions:
16
17- `cargo check -p <crate>` for fast iteration; `bun bd` builds and links everything.
18- Don't `.unwrap()` a fallible path that user input or the OS can hit at runtime — return the error. `.unwrap()` is for invariants you can prove.
19- The C ABI / syscall boundary uses `bun_sys::Maybe<T>` (= `Result<T, bun_sys::Error>`); ordinary Rust code uses `Result<T, E>` with `?`.
20- Each crate defines its own `Error` enum (a `thiserror::Error` at `<crate>/error.rs`, re-exported as `crate::Error` + `crate::Result`). Errno codes nest via `Sys(#[from] bun_errno::SystemErrno)`; OOM via `Alloc(#[from] bun_alloc::AllocError)`. `bun_sys::Error` is the rich syscall error (errno + syscall tag + path); `From<bun_sys::Error> for bun_errno::SystemErrno` exists for `?`-chaining.
21- NEVER add comments to deleted code blocks.
22- Do not add comments that reference context from the transcript.
23- Avoid adding comments where not necessary.
24
25## Prefer `bun_core` / `bun_sys` over `std`
26
27The `std` equivalents either lose OS error info, allocate where we have pools,
28or don't match the cross-platform behavior the runtime needs.
29
30| Instead of | Use |
31| --------------------------------------- | ------------------------------------------------------------------------------------ |
32| `std::fs::File` | `bun_sys::File` (owns the fd; closes on `Drop`) |
33| `std::fs::read` / `write` | `bun_sys::File::read_from` / `File::create` + `write_all` |
34| `std::path::Path::join` | `bun_paths::resolve_path::join` / `join_string_buf` |
35| `std::path::Path::parent`/`file_name` | `bun_paths::dirname` / `bun_paths::basename` |
36| `std::env::var` | `bun_core::env_var::*::get()` (typed + cached) |
37| `String::from_utf8` for JS-visible strs | `bun_core::String::clone_utf8` / `borrow_utf8` |
38| `&str` operations on byte slices | `bun_core::strings::*` (SIMD-backed `&[u8]` ops) |
39| `eprintln!` for debug logging | `bun_core::declare_scope!` + `scoped_log!` |
40| `std::process::Command` | `bun_core::util::spawn_sync_inherit` (CLI helpers) or `bun_spawn_sys` (full control) |
41| `Box::new` + raw ptr round-trip | `bun_core::heap::{into_raw, take, destroy}` |
42
43## `bun_sys` — System Calls (`src/sys/`)
44
45Syscall wrappers preserve errno via `Maybe<T> = Result<T, bun_sys::Error>`.
46
47```rust
48use bun_sys::{File, Fd, O};
49
50let file = File::openat(Fd::cwd(), b"path/to/file", O::RDONLY, 0)?;
51let mut buf = vec![0u8; 4096];
52let n = file.read_all(&mut buf)?; // loops until EOF or full
53// `file` closes on Drop.
54```
55
56Key types and functions:
57
58- `Fd` (`bun_core::Fd`, re-exported) — cross-platform file descriptor. `Fd::cwd()`, `Fd::stdin()/stdout()/stderr()`, `fd.close()`.
59- `File::open(path: &ZStr, flags, mode)` / `File::openat(dir: Fd, path: &[u8], flags, mode)` / `File::make_open(...)` (creates parent dirs) / `File::create(dir, path, truncate)`
60- `file.read(buf)` / `read_all(buf)` / `read_to_end()` / `read_to_end_small()` / `write(buf)` / `write_all(buf)`
61- `bun_sys::open`, `read`, `write`, `pread`, `pwrite`, `stat`, `fstat`, `lstat`, `mkdir`, `unlink`, `rename`, `symlink`, `chmod` — free fns over `Fd`
62- Open flags: `bun_sys::O::RDONLY`, `O::WRONLY | O::CREAT | O::TRUNC`, etc.
63
64`bun_sys::Error` carries `errno`, `syscall: Tag`, `path: Box<[u8]>`. Convert
65to a JS exception via `bun_sys_jsc::ErrorJsc::to_js`:
66
67```rust
68use bun_sys_jsc::ErrorJsc;
69match File::openat(Fd::cwd(), path, O::RDONLY, 0) {
70 Ok(f) => f,
71 Err(err) => return Ok(err.to_js(global)?),
72}
73// Internally: err.to_system_error().to_error_instance(global)
74```
75
76## Strings (`bun_core::String` and `bun_core::strings`)
77
78`bun_core::String` is the FFI-compatible 5-variant tagged union shared with C++
79(`BunString` in `BunString.cpp`). It bridges Rust and JSC and can hold a
80`WTFStringImpl` (Latin-1 or UTF-16). **Latin-1 is NOT UTF-8** — bytes 128–255
81are single chars in Latin-1 but invalid UTF-8 — so converting either direction
82requires a real encoder, not a cast.
83
84```rust
85use bun_core::String;
86
87let s = String::clone_utf8(utf8_bytes); // copies into a WTFStringImpl
88let s = String::borrow_utf8(utf8_bytes); // no copy; caller keeps slice alive
89let s = String::static_(b"literal"); // 'static slice, never freed
90
91let utf8: ZigStringSlice = s.to_utf8(); // ref-holding view; falls back to allocating a copy
92let owned: Vec<u8> = s.to_utf8_bytes();
93```
94
95To/from JS values, use the `bun_jsc::StringJsc` extension trait:
96
97```rust
98use bun_jsc::StringJsc;
99let js: JSValue = s.to_js(global)?;
100let s = bun_core::String::from_js(value, global)?;
101let err = s.to_error_instance(global);
102```
103
104`bun_core::strings` is the SIMD-backed `&[u8]` toolkit. Use it instead of
105`std::str` / `std::iter` for searching and comparing byte slices:
106
107```rust
108use bun_core::strings;
109
110strings::index_of(haystack, needle) // Option<usize>
111strings::contains(haystack, needle) // bool
112strings::eql(a, b) // bool
113strings::starts_with(s, prefix) // bool
114strings::ends_with(s, suffix) // bool
115strings::has_prefix_comptime(s, b"x") // 'static comparand
116strings::has_suffix_comptime(s, b"x")
117strings::first_non_ascii(s) // Option<u32>
118strings::to_utf16_alloc(...) // encoding conversions
119```
120
121## Paths (`bun_paths`)
122
123Path helpers operate on `&[u8]` and are platform-parameterized via the
124`Platform` const-generic (`Posix`, `Windows`, `Loose`, `Nt`; `platform::Auto`
125picks the host). Never use `std::path` for runtime path logic.
126
127```rust
128use bun_paths::{dirname, basename};
129use bun_paths::resolve_path::{self, platform};
130
131let dir = dirname(path); // Option<&[u8]>
132let name = basename(path); // &[u8]
133let joined = resolve_path::join::<platform::Auto>(&[a, b]); // &'static [u8] (threadlocal buf)
134let joined = resolve_path::join_string_buf::<platform::Auto>(&mut buf, &[a, b]); // caller buf
135let rel = resolve_path::relative(from, to);
136```
137
138Use the path-buffer pool to avoid 64 KB stack allocations on Windows
139(`PathBuffer` is `[u8; PATH_MAX_BYTES]`, ~64 KB on Windows):
140
141```rust
142use bun_paths::path_buffer_pool;
143
144let mut buf = path_buffer_pool::get(); // PoolGuard<PathBuffer>, returns to pool on Drop
145let joined = resolve_path::join_string_buf::<platform::Auto>(&mut *buf, &[a, b]);
146```
147
148`bun_paths::os_path_buffer_pool` selects the wide (`u16`) variant on Windows
149and the narrow (`u8`) variant on POSIX.
150
151## URL Parsing (`bun_jsc::URL`)
152
153WHATWG-compliant, backed by WebKit's URL parser. Returns `None` for invalid input.
154
155```rust
156use bun_jsc::URL;
157
158let url = URL::from_utf8(href)?; // Option<NonNull<URL>>
159// caller owns the C++ object — destroy it when done:
160// unsafe { URL::destroy(url.as_ptr()) }
161
162url.protocol() // bun_core::String
163url.pathname() // bun_core::String
164url.host() // bun_core::String — the hostname WITHOUT the port (opposite of JS `host`!)
165url.port() // u32 (u32::MAX = unset; otherwise u16 range)
166```
167
168`URL::href_from_js`, `URL::file_url_from_string`, `URL::path_from_file_url`
169do whole-string conversions. The JSC-free shim `bun_url::whatwg::URL` exposes
170`hostname()`, which returns the host WITH the port (also the opposite of JS
171`hostname`) — so `bun_jsc::URL::host` and `bun_url::whatwg::URL::hostname`
172are effectively swapped relative to their JS namesakes.
173
174## MIME Types (`bun_http_types::MimeType`)
175
176```rust
177use bun_http_types::{MimeType, mime_type};
178
179let mime = mime_type::by_extension(b"html"); // MimeType
180let mime = mime_type::by_extension_no_default(b"xyz"); // Option<MimeType>
181
182mime.category // Category::Javascript | Css | Html | Json | Image | Text | Wasm | ...
183mime.category.is_text_like()
184```
185
186Common constants: `JAVASCRIPT`, `JSON`, `HTML`, `CSS`, `TEXT`, `WASM`, `ICO`, `OTHER`.
187
188## Memory & Allocators
189
190The `#[global_allocator]` is mimalloc (or `std::alloc::System` under
191`cfg(bun_asan)`), so plain `Box`/`Vec`/`String` already use it. When pairing
192with C/C++ that may free the bytes, route through `bun_alloc::default_alloc`
193rather than `mi_*` directly — under ASAN the global allocator is libc's, so a
194`mi_free`/`mi_usable_size` on `Box`-owned memory is an allocator mismatch.
195
196OOM handling: do not let a runtime OOM unwind into FFI. Use
197`bun_core::handle_oom` (or the `.unwrap_or_oom()` extension) to convert
198`Result<T, AllocError>` into a controlled crash:
199
200```rust
201use bun_core::{handle_oom, UnwrapOrOom};
202let buf = handle_oom(allocator.alloc(size));
203let v = vec.try_reserve(n).unwrap_or_oom();
204```
205
206Heap round-trips that need to cross FFI use `bun_core::heap`:
207
208```rust
209use bun_core::heap;
210let raw: *mut T = heap::into_raw(Box::new(value)); // hand ownership to C
211let boxed: Box<T> = unsafe { heap::take(raw) }; // reclaim ownership
212unsafe { heap::destroy(raw) }; // reclaim + drop in one step
213```
214
215**Arena gotcha:** values allocated in `bun_alloc::MimallocArena` (the AST
216allocator and similar) do **not** run `Drop` when the arena resets — the
217backing pages are bulk-freed. If a type owns a heap allocation, refcount, or
218fd, free it explicitly before the arena resets. Don't rely on `Drop` for
219correctness in arena-backed code.
220
221## Environment Variables (`bun_core::env_var`)
222
223Typed, cached accessors. Each known env var is a module with a `get()`
224returning the right type (`Option<...>` if no default).
225
226```rust
227use bun_core::env_var;
228
229env_var::HOME::get() // Option<&[u8]>
230env_var::CI::get() // bool (has default)
231env_var::BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS::get() // u64 (has default)
232```
233
234## Logging (`bun_core::output`)
235
236Scoped debug logging. Declare a scope once per module; gate with
237`BUN_DEBUG_<SCOPE>=1` at runtime; the body dead-strips in release builds.
238
239```rust
240bun_core::declare_scope!(my_feature, hidden); // hidden: opt-in via BUN_DEBUG_my_feature=1
241// or `visible` to log by default in debug builds
242
243bun_core::scoped_log!(my_feature, "processing {} items", count);
244```
245
246User-facing colored output (auto-detects TTY, strips ANSI when piped):
247
248```rust
249bun_core::pretty!("<green>success<r>: {}\n", msg);
250bun_core::prettyln!("done");
251bun_core::pretty_errorln!("<red>error<r>: {}", msg);
252```
253
254## Spawning Subprocesses
255
256For simple inherit-stdio CLI helpers:
257
258```rust
259use bun_core::util::spawn_sync_inherit;
260let status = spawn_sync_inherit(&[b"git", b"status"])?;
261```
262
263For full control (pipes, custom env, posix_spawn flags) use `bun_spawn_sys`
264(`src/spawn_sys/`). The runtime `Bun.spawn` implementation lives in
265`src/runtime/api/bun/{spawn.rs, process.rs, subprocess.rs}` — look there for
266the JS-facing path.
267
268## JSC Interop & FFI Safety
269
270These are the patterns that trip people up. Get them wrong and you get
271crashes that only reproduce under load or in CI.
272
273### Pointer provenance at FFI boundaries
274
275If a callback may free `self` (close, error, GC finalize), do **not**
276materialize `&self`/`&mut self` at the boundary — a `&self`-derived raw
277pointer carries `SharedReadOnly` provenance, and `Box::from_raw`/dealloc
278through it is UB. Pass and dispatch off `*mut Self` until the body proves
279ownership. `src/io/PipeWriter.rs`'s `impl_streaming_writer_parent!` macro
280encodes the three modes:
281
282- `borrow = mut` — body forms `&mut *this`; safe when nothing re-enters
283- `borrow = shared` — body forms `&*this`; safe when re-entrant code only needs `&Self`
284- `borrow = ptr` — body calls `Self::method(this, ..)` with `this: *mut Self`; required when the callback may free `self`
285
286### `Strong` / `Weak` JS handles
287
288`bun_jsc::Strong` keeps a JS value alive; it is `!Send`/`!Sync` and must be
289created and dropped on the JS thread.
290
291```rust
292use bun_jsc::Strong;
293let strong = Strong::create(value, global);
294let v: JSValue = strong.get();
295// drop(strong) releases the GC handle
296```
297
298`bun_jsc::Weak<T>` is the GC-cleared variant. For raw values without a `Strong`
299wrapper, `JSValue::protect()` / `unprotect()` and `ensure_still_alive()` are
300available, but `Strong` is preferred — it can't be forgotten or unbalanced.
301
302### Refcount transfer on `to_js()` / `create()`
303
304A `to_js()` / `create()` that returns a wrapped pointer **transfers** the
305caller's `+1` to the JS wrapper. Do not `ref()` again before the return; the
306finalizer derefs once. The leak-or-UAF symptoms of getting this wrong are
307distinctive: an extra `ref()` leaks until process exit; a missing `ref()` on a
308non-transferring path UAFs at GC.
309
310### Cross-thread string hazards
311
312`AtomString`s live in a per-thread table. Never deref one from another thread —
313it trips `wasRemoved` in `AtomStringImpl::remove()`. If a `bun_core::String`
314may be dropped from a non-JS thread (HTTP worker, threadpool, dying VM), build
315it via `String::clone_utf8` (a plain `WTFStringImpl` with an atomic refcount),
316not from an interned/atomized JS string. See the comment in
317`src/runtime/webcore/fetch/FetchTasklet.rs` near `Response::init` for the
318canonical example of this bug class and its fix.
319
320## Common Patterns
321
322```rust
323// Read a file, return JS error on failure
324let contents = match bun_sys::File::openat(Fd::cwd(), path, O::RDONLY, 0)
325 .and_then(|f| f.read_to_end())
326{
327 Ok(bytes) => bytes,
328 Err(err) => return Ok(err.to_js(global)?),
329};
330
331// Heap-allocated FFI handle with explicit lifecycle
332let raw = bun_core::heap::into_raw(Box::new(MyHandle::new()));
333register_with_c(raw);
334// ... later, in the matching teardown callback:
335unsafe { bun_core::heap::destroy(raw) };
336
337// Hashing
338bun_wyhash::hash(bytes) // u64
339bun_wyhash::hash_with_seed(seed, bytes)
340```
341
@@ −1 +1 @@
1−To run tests:
1+## Rust
22
3−```sh
4−bun bd test <...test file>
5−```
3+`src/` is a Cargo workspace (rooted at the repo's top-level `Cargo.toml`, ~200
4+member crates). The runtime is built as `libbun_rust.a` via `cargo build -p
5+bun_bin` (driven by `scripts/build/rust.ts`). Key crates:
66
7−To run a command with your debug build of Bun:
7+- `bun_core` (`src/bun_core/`) — strings, formatting, logging, env vars, allocator/heap helpers, the foundation everything else uses
8+- `bun_sys` (`src/sys/`) — cross-platform syscall wrappers (`File`, `Fd`, `Dir`, `Error`)
9+- `bun_paths` (`src/paths/`) — path joining/normalization, the path-buffer pool
10+- `bun_jsc` (`src/jsc/`) — JSC value types, `Strong`/`Weak`, FFI imports, `URL`
11+- `bun_runtime` (`src/runtime/`) — JS-visible APIs (server, fetch, node compat, crypto)
12+- `bun_js_parser`, `bun_js_printer`, `bun_resolver`, `bun_bundler`, `bun_install`, `bun_collections`, `bun_threading`, `bun_alloc` — the rest of the pipeline
13+- `bun_bin` (`src/bun_bin/`) — the staticlib root that `cargo build` links
814
9−```sh
10−bun bd <...cmd>
11−```
15+Conventions:
1216
13−Note that compiling Bun may take up to 2.5 minutes. It is slow!
17+- `cargo check -p <crate>` for fast iteration; `bun bd` builds and links everything.
18+- Don't `.unwrap()` a fallible path that user input or the OS can hit at runtime — return the error. `.unwrap()` is for invariants you can prove.
19+- The C ABI / syscall boundary uses `bun_sys::Maybe<T>` (= `Result<T, bun_sys::Error>`); ordinary Rust code uses `Result<T, E>` with `?`.
20+- Each crate defines its own `Error` enum (a `thiserror::Error` at `<crate>/error.rs`, re-exported as `crate::Error` + `crate::Result`). Errno codes nest via `Sys(#[from] bun_errno::SystemErrno)`; OOM via `Alloc(#[from] bun_alloc::AllocError)`. `bun_sys::Error` is the rich syscall error (errno + syscall tag + path); `From<bun_sys::Error> for bun_errno::SystemErrno` exists for `?`-chaining.
21+- NEVER add comments to deleted code blocks.
22+- Do not add comments that reference context from the transcript.
23+- Avoid adding comments where not necessary.
1424
15−**CRITICAL**: Do not use `bun test` to run tests. It will not have your changes. `bun bd test <...test file>` is the correct command, which compiles your code automatically.
25+## Prefer `bun_core` / `bun_sys` over `std`
1626
17−## Testing style
27+The `std` equivalents either lose OS error info, allocate where we have pools,
28+or don't match the cross-platform behavior the runtime needs.
1829
19−Use `bun:test` with files that end in `*.test.{ts,js,jsx,tsx,mjs,cjs}`. If it's a test/js/node/test/{parallel,sequential}/\*.js without a .test.extension, use `bun bd <file>` instead of `bun bd test <file>` since those expect exit code 0 and don't use bun's test runner.
30+| Instead of | Use |
31+| --------------------------------------- | ------------------------------------------------------------------------------------ |
32+| `std::fs::File` | `bun_sys::File` (owns the fd; closes on `Drop`) |
33+| `std::fs::read` / `write` | `bun_sys::File::read_from` / `File::create` + `write_all` |
34+| `std::path::Path::join` | `bun_paths::resolve_path::join` / `join_string_buf` |
35+| `std::path::Path::parent`/`file_name` | `bun_paths::dirname` / `bun_paths::basename` |
36+| `std::env::var` | `bun_core::env_var::*::get()` (typed + cached) |
37+| `String::from_utf8` for JS-visible strs | `bun_core::String::clone_utf8` / `borrow_utf8` |
38+| `&str` operations on byte slices | `bun_core::strings::*` (SIMD-backed `&[u8]` ops) |
39+| `eprintln!` for debug logging | `bun_core::declare_scope!` + `scoped_log!` |
40+| `std::process::Command` | `bun_core::util::spawn_sync_inherit` (CLI helpers) or `bun_spawn_sys` (full control) |
41+| `Box::new` + raw ptr round-trip | `bun_core::heap::{into_raw, take, destroy}` |
2042
21−- **Do not write flaky tests**. Unless explicitly asked, **never wait for time to pass in tests**. Always wait for the condition to be met instead of waiting for an arbitrary amount of time. **Never use hardcoded port numbers**. Always use `port: 0` to get a random port.
22−- **Prefer concurrent tests over sequential tests**: When multiple tests in the same file spawn processes or write files, make them concurrent with `test.concurrent` or `describe.concurrent` unless it's very difficult to make them concurrent.
43+## `bun_sys` — System Calls (`src/sys/`)
2344
24−### Spawning processes
45+Syscall wrappers preserve errno via `Maybe<T> = Result<T, bun_sys::Error>`.
2546
26−#### Spawning Bun in tests
47+```rust
48+use bun_sys::{File, Fd, O};
2749
28−When spawning Bun processes, use `bunExe` and `bunEnv` from `harness`. This ensures the same build of Bun is used to run the test and ensures debug logging is silenced.
50+let file = File::openat(Fd::cwd(), b"path/to/file", O::RDONLY, 0)?;
51+let mut buf = vec![0u8; 4096];
52+let n = file.read_all(&mut buf)?; // loops until EOF or full
53+// `file` closes on Drop.
54+```
2955
30−##### Use `-e` for single-file tests
56+Key types and functions:
3157
32−```ts
33−import { bunEnv, bunExe, tempDir } from "harness";
34−import { test, expect } from "bun:test";
58+- `Fd` (`bun_core::Fd`, re-exported) — cross-platform file descriptor. `Fd::cwd()`, `Fd::stdin()/stdout()/stderr()`, `fd.close()`.
59+- `File::open(path: &ZStr, flags, mode)` / `File::openat(dir: Fd, path: &[u8], flags, mode)` / `File::make_open(...)` (creates parent dirs) / `File::create(dir, path, truncate)`
60+- `file.read(buf)` / `read_all(buf)` / `read_to_end()` / `read_to_end_small()` / `write(buf)` / `write_all(buf)`
61+- `bun_sys::open`, `read`, `write`, `pread`, `pwrite`, `stat`, `fstat`, `lstat`, `mkdir`, `unlink`, `rename`, `symlink`, `chmod` — free fns over `Fd`
62+- Open flags: `bun_sys::O::RDONLY`, `O::WRONLY | O::CREAT | O::TRUNC`, etc.
3563
36−test("single-file test spawns a Bun process", async () => {
37− await using proc = Bun.spawn({
38− cmd: [bunExe(), "-e", "console.log('Hello, world!')"],
39− env: bunEnv,
40− });
64+`bun_sys::Error` carries `errno`, `syscall: Tag`, `path: Box<[u8]>`. Convert
65+to a JS exception via `bun_sys_jsc::ErrorJsc::to_js`:
4166
42− const [stdout, stderr, exitCode] = await Promise.all([
43− proc.stdout.text(),
44− proc.stderr.text(),
45− proc.exited,
46− ]);
67+```rust
68+use bun_sys_jsc::ErrorJsc;
69+match File::openat(Fd::cwd(), path, O::RDONLY, 0) {
70+ Ok(f) => f,
71+ Err(err) => return Ok(err.to_js(global)?),
72+}
73+// Internally: err.to_system_error().to_error_instance(global)
74+```
4775
48− expect(stderr).toBe("");
49− expect(stdout).toBe("Hello, world!\n");
50− expect(exitCode).toBe(0);
51−});
76+## Strings (`bun_core::String` and `bun_core::strings`)
77+
78+`bun_core::String` is the FFI-compatible 5-variant tagged union shared with C++
79+(`BunString` in `BunString.cpp`). It bridges Rust and JSC and can hold a
80+`WTFStringImpl` (Latin-1 or UTF-16). **Latin-1 is NOT UTF-8** — bytes 128–255
81+are single chars in Latin-1 but invalid UTF-8 — so converting either direction
82+requires a real encoder, not a cast.
83+
84+```rust
85+use bun_core::String;
86+
87+let s = String::clone_utf8(utf8_bytes); // copies into a WTFStringImpl
88+let s = String::borrow_utf8(utf8_bytes); // no copy; caller keeps slice alive
89+let s = String::static_(b"literal"); // 'static slice, never freed
90+
91+let utf8: ZigStringSlice = s.to_utf8(); // ref-holding view; falls back to allocating a copy
92+let owned: Vec<u8> = s.to_utf8_bytes();
5293 ```
5394
54−##### When multi-file tests are required:
95+To/from JS values, use the `bun_jsc::StringJsc` extension trait:
5596
56−```ts
57−import { bunEnv, bunExe, tempDir } from "harness";
58−import { test, expect } from "bun:test";
97+```rust
98+use bun_jsc::StringJsc;
99+let js: JSValue = s.to_js(global)?;
100+let s = bun_core::String::from_js(value, global)?;
101+let err = s.to_error_instance(global);
102+```
59103
60−test("multi-file test spawns a Bun process", async () => {
61− // If a test MUST use multiple files:
62− using dir = tempDir("my-test-prefix", {
63− "my.fixture.ts": `
64− import { foo } from "./foo.ts";
65− foo();
66− `,
67− "foo.ts": `
68− export function foo() {
69− console.log("Hello, world!");
70− }
71− `,
72− });
104+`bun_core::strings` is the SIMD-backed `&[u8]` toolkit. Use it instead of
105+`std::str` / `std::iter` for searching and comparing byte slices:
73106
74− await using proc = Bun.spawn({
75− cmd: [bunExe(), "my.fixture.ts"],
76− env: bunEnv,
77− cwd: String(dir),
78− });
107+```rust
108+use bun_core::strings;
79109
80− const [stdout, stderr, exitCode] = await Promise.all([
110+strings::index_of(haystack, needle) // Option<usize>
111+strings::contains(haystack, needle) // bool
112+strings::eql(a, b) // bool
113+strings::starts_with(s, prefix) // bool
114+strings::ends_with(s, suffix) // bool
115+strings::has_prefix_comptime(s, b"x") // 'static comparand
116+strings::has_suffix_comptime(s, b"x")
117+strings::first_non_ascii(s) // Option<u32>
118+strings::to_utf16_alloc(...) // encoding conversions
119+```
81120
82− // ReadableStream in Bun supports:
83− // - `await stream.text()`
84− // - `await stream.json()`
85− // - `await stream.bytes()`
86− // - `await stream.blob()`
87− proc.stdout.text(),
88− proc.stderr.text(),
121+## Paths (`bun_paths`)
89122
90− proc.exitCode,
91− ]);
123+Path helpers operate on `&[u8]` and are platform-parameterized via the
124+`Platform` const-generic (`Posix`, `Windows`, `Loose`, `Nt`; `platform::Auto`
125+picks the host). Never use `std::path` for runtime path logic.
92126
93− expect(stdout).toBe("Hello, world!");
94− expect(stderr).toBe("");
95− expect(exitCode).toBe(0);
127+```rust
128+use bun_paths::{dirname, basename};
129+use bun_paths::resolve_path::{self, platform};
130+
131+let dir = dirname(path); // Option<&[u8]>
132+let name = basename(path); // &[u8]
133+let joined = resolve_path::join::<platform::Auto>(&[a, b]); // &'static [u8] (threadlocal buf)
134+let joined = resolve_path::join_string_buf::<platform::Auto>(&mut buf, &[a, b]); // caller buf
135+let rel = resolve_path::relative(from, to);
96136 ```
97137
98−When a test file spawns a Bun process, we like for that file to end in `*-fixture.ts`. This is a convention that helps us identify the file as a test fixture and not a test itself.
138+Use the path-buffer pool to avoid 64 KB stack allocations on Windows
139+(`PathBuffer` is `[u8; PATH_MAX_BYTES]`, ~64 KB on Windows):
99140
100−Generally, `await using` or `using` is a good idea to ensure proper resource cleanup. This works in most Bun APIs like Bun.listen, Bun.connect, Bun.spawn, Bun.serve, etc.
141+```rust
142+use bun_paths::path_buffer_pool;
101143
102−#### Async/await in tests
144+let mut buf = path_buffer_pool::get(); // PoolGuard<PathBuffer>, returns to pool on Drop
145+let joined = resolve_path::join_string_buf::<platform::Auto>(&mut *buf, &[a, b]);
146+```
103147
104−Prefer async/await over callbacks.
148+`bun_paths::os_path_buffer_pool` selects the wide (`u16`) variant on Windows
149+and the narrow (`u8`) variant on POSIX.
105150
106−When callbacks must be used and it's just a single callback, use `Promise.withResolvers` to create a promise that can be resolved or rejected from a callback.
151+## URL Parsing (`bun_jsc::URL`)
107152
108−```ts
109−const ws = new WebSocket("ws://localhost:8080");
110−const { promise, resolve, reject } = Promise.withResolvers<void>(); // Can specify any type here for resolution value
111−ws.onopen = resolve;
112−ws.onclose = reject;
113−await promise;
153+WHATWG-compliant, backed by WebKit's URL parser. Returns `None` for invalid input.
154+
155+```rust
156+use bun_jsc::URL;
157+
158+let url = URL::from_utf8(href)?; // Option<NonNull<URL>>
159+// caller owns the C++ object — destroy it when done:
160+// unsafe { URL::destroy(url.as_ptr()) }
161+
162+url.protocol() // bun_core::String
163+url.pathname() // bun_core::String
164+url.host() // bun_core::String — the hostname WITHOUT the port (opposite of JS `host`!)
165+url.port() // u32 (u32::MAX = unset; otherwise u16 range)
114166 ```
115167
116−If it's several callbacks, it's okay to use callbacks. We aren't a stickler for this.
168+`URL::href_from_js`, `URL::file_url_from_string`, `URL::path_from_file_url`
169+do whole-string conversions. The JSC-free shim `bun_url::whatwg::URL` exposes
170+`hostname()`, which returns the host WITH the port (also the opposite of JS
171+`hostname`) — so `bun_jsc::URL::host` and `bun_url::whatwg::URL::hostname`
172+are effectively swapped relative to their JS namesakes.
117173
118−### No timeouts
174+## MIME Types (`bun_http_types::MimeType`)
119175
120−**CRITICAL**: Do not set a timeout on tests. Bun already has timeouts.
176+```rust
177+use bun_http_types::{MimeType, mime_type};
121178
122−### Use port 0 to get a random port
179+let mime = mime_type::by_extension(b"html"); // MimeType
180+let mime = mime_type::by_extension_no_default(b"xyz"); // Option<MimeType>
123181
124−Most APIs in Bun support `port: 0` to get a random port. Never hardcode ports. Avoid using your own random port number function.
182+mime.category // Category::Javascript | Css | Html | Json | Image | Text | Wasm | ...
183+mime.category.is_text_like()
184+```
125185
126−### Creating temporary files
186+Common constants: `JAVASCRIPT`, `JSON`, `HTML`, `CSS`, `TEXT`, `WASM`, `ICO`, `OTHER`.
127187
128−Use `tempDirWithFiles` to create a temporary directory with files.
188+## Memory & Allocators
129189
130−```ts
131−import { tempDir } from "harness";
132−import path from "node:path";
190+The `#[global_allocator]` is mimalloc (or `std::alloc::System` under
191+`cfg(bun_asan)`), so plain `Box`/`Vec`/`String` already use it. When pairing
192+with C/C++ that may free the bytes, route through `bun_alloc::default_alloc`
193+rather than `mi_*` directly — under ASAN the global allocator is libc's, so a
194+`mi_free`/`mi_usable_size` on `Box`-owned memory is an allocator mismatch.
133195
134−test("creates a temporary directory with files", () => {
135− using dir = tempDir("my-test-prefix", {
136− "file.txt": "Hello, world!",
137− });
196+OOM handling: do not let a runtime OOM unwind into FFI. Use
197+`bun_core::handle_oom` (or the `.unwrap_or_oom()` extension) to convert
198+`Result<T, AllocError>` into a controlled crash:
138199
139− expect(await Bun.file(path.join(String(dir), "file.txt")).text()).toBe(
140− "Hello, world!",
141− );
142−});
200+```rust
201+use bun_core::{handle_oom, UnwrapOrOom};
202+let buf = handle_oom(allocator.alloc(size));
203+let v = vec.try_reserve(n).unwrap_or_oom();
143204 ```
144205
145−### Strings
206+Heap round-trips that need to cross FFI use `bun_core::heap`:
146207
147−To create a repetitive string, use `Buffer.alloc(count, fill).toString()` instead of `"A".repeat(count)`. "".repeat is very slow in debug JavaScriptCore builds.
208+```rust
209+use bun_core::heap;
210+let raw: *mut T = heap::into_raw(Box::new(value)); // hand ownership to C
211+let boxed: Box<T> = unsafe { heap::take(raw) }; // reclaim ownership
212+unsafe { heap::destroy(raw) }; // reclaim + drop in one step
213+```
148214
149−### Test Organization
215+**Arena gotcha:** values allocated in `bun_alloc::MimallocArena` (the AST
216+allocator and similar) do **not** run `Drop` when the arena resets — the
217+backing pages are bulk-freed. If a type owns a heap allocation, refcount, or
218+fd, free it explicitly before the arena resets. Don't rely on `Drop` for
219+correctness in arena-backed code.
150220
151−- Use `describe` blocks for grouping related tests
152−- **Add tests to the existing test file for the code you're changing** — do not create a new file. Tests are organized by module (e.g., `/test/js/bun/`, `/test/js/node/`, `/test/js/web/`).
153−- `/test/regression/issue/${issueNumber}.test.ts` is **only** for bugs that have a GitHub issue number **and** are true regressions (worked in a previous release, then broke). An issue number alone does not qualify — if it was never correct, put the test in the module's existing test file instead.
154−- Integration tests are in `/test/integration/`
221+## Environment Variables (`bun_core::env_var`)
155222
156−### Nested/complex object equality
223+Typed, cached accessors. Each known env var is a module with a `get()`
224+returning the right type (`Option<...>` if no default).
157225
158−Prefer usage of `.toEqual` rather than many `.toBe` assertions for nested or complex objects.
226+```rust
227+use bun_core::env_var;
159228
160−<example>
229+env_var::HOME::get() // Option<&[u8]>
230+env_var::CI::get() // bool (has default)
231+env_var::BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS::get() // u64 (has default)
232+```
161233
162−BAD (try to avoid doing this):
234+## Logging (`bun_core::output`)
163235
164−```ts
165−expect(result).toHaveLength(3);
166−expect(result[0].optional).toBe(null);
167−expect(result[1].optional).toBe("middle-value"); // CRITICAL: middle item's value must be preserved
168−expect(result[2].optional).toBe(null);
236+Scoped debug logging. Declare a scope once per module; gate with
237+`BUN_DEBUG_<SCOPE>=1` at runtime; the body dead-strips in release builds.
238+
239+```rust
240+bun_core::declare_scope!(my_feature, hidden); // hidden: opt-in via BUN_DEBUG_my_feature=1
241+// or `visible` to log by default in debug builds
242+
243+bun_core::scoped_log!(my_feature, "processing {} items", count);
169244 ```
170245
171−**GOOD (always prefer this):**
246+User-facing colored output (auto-detects TTY, strips ANSI when piped):
172247
173−```ts
174−expect(result).toEqual([
175− { optional: null },
176− { optional: "middle-value" }, // CRITICAL: middle item's value must be preserved
177− { optional: null },
178−]);
248+```rust
249+bun_core::pretty!("<green>success<r>: {}\n", msg);
250+bun_core::prettyln!("done");
251+bun_core::pretty_errorln!("<red>error<r>: {}", msg);
179252 ```
180253
181−</example>
254+## Spawning Subprocesses
182255
183−### Common Imports from `harness`
256+For simple inherit-stdio CLI helpers:
184257
185−```ts
186−import {
187− bunExe, // Path to Bun executable
188− bunEnv, // Environment variables for Bun
189− tempDirWithFiles, // Create temporary test directories with files
190− tmpdirSync, // Create empty temporary directory
191− isMacOS, // Platform checks
192− isWindows,
193− isPosix,
194− gcTick, // Trigger garbage collection
195− withoutAggressiveGC, // Disable aggressive GC for performance tests
196−} from "harness";
258+```rust
259+use bun_core::util::spawn_sync_inherit;
260+let status = spawn_sync_inherit(&[b"git", b"status"])?;
197261 ```
198262
199−### Error Testing
263+For full control (pipes, custom env, posix_spawn flags) use `bun_spawn_sys`
264+(`src/spawn_sys/`). The runtime `Bun.spawn` implementation lives in
265+`src/runtime/api/bun/{spawn.rs, process.rs, subprocess.rs}` — look there for
266+the JS-facing path.
200267
201−Always check exit codes and test error scenarios:
268+## JSC Interop & FFI Safety
202269
203−```ts
204−test("handles errors", async () => {
205− await using proc = Bun.spawn({
206− cmd: [bunExe(), "run", "invalid.js"],
207− env: bunEnv,
208− });
270+These are the patterns that trip people up. Get them wrong and you get
271+crashes that only reproduce under load or in CI.
209272
210− const exitCode = await proc.exited;
211− expect(exitCode).not.toBe(0);
273+### Pointer provenance at FFI boundaries
212274
213− // For synchronous errors
214− expect(() => someFunction()).toThrow("Expected error message");
215−});
275+If a callback may free `self` (close, error, GC finalize), do **not**
276+materialize `&self`/`&mut self` at the boundary — a `&self`-derived raw
277+pointer carries `SharedReadOnly` provenance, and `Box::from_raw`/dealloc
278+through it is UB. Pass and dispatch off `*mut Self` until the body proves
279+ownership. `src/io/PipeWriter.rs`'s `impl_streaming_writer_parent!` macro
280+encodes the three modes:
281+
282+- `borrow = mut` — body forms `&mut *this`; safe when nothing re-enters
283+- `borrow = shared` — body forms `&*this`; safe when re-entrant code only needs `&Self`
284+- `borrow = ptr` — body calls `Self::method(this, ..)` with `this: *mut Self`; required when the callback may free `self`
285+
286+### `Strong` / `Weak` JS handles
287+
288+`bun_jsc::Strong` keeps a JS value alive; it is `!Send`/`!Sync` and must be
289+created and dropped on the JS thread.
290+
291+```rust
292+use bun_jsc::Strong;
293+let strong = Strong::create(value, global);
294+let v: JSValue = strong.get();
295+// drop(strong) releases the GC handle
216296 ```
217297
218−### Avoid dynamic import & require
298+`bun_jsc::Weak<T>` is the GC-cleared variant. For raw values without a `Strong`
299+wrapper, `JSValue::protect()` / `unprotect()` and `ensure_still_alive()` are
300+available, but `Strong` is preferred — it can't be forgotten or unbalanced.
219301
220−**Only** use dynamic import or require when the test is specifically testing something relataed to dynamic import or require. Otherwise, **always use module-scope import statements**.
302+### Refcount transfer on `to_js()` / `create()`
221303
222−**BAD, do not do this**:
304+A `to_js()` / `create()` that returns a wrapped pointer **transfers** the
305+caller's `+1` to the JS wrapper. Do not `ref()` again before the return; the
306+finalizer derefs once. The leak-or-UAF symptoms of getting this wrong are
307+distinctive: an extra `ref()` leaks until process exit; a missing `ref()` on a
308+non-transferring path UAFs at GC.
223309
224−```ts
225−test("foo", async () => {
226− // BAD: Unnecessary usage of dynamic import.
227− const { readFile } = await import("node:fs");
310+### Cross-thread string hazards
228311
229− expect(await readFile("ok.txt")).toBe("");
230−});
231−```
312+`AtomString`s live in a per-thread table. Never deref one from another thread —
313+it trips `wasRemoved` in `AtomStringImpl::remove()`. If a `bun_core::String`
314+may be dropped from a non-JS thread (HTTP worker, threadpool, dying VM), build
315+it via `String::clone_utf8` (a plain `WTFStringImpl` with an atomic refcount),
316+not from an interned/atomized JS string. See the comment in
317+`src/runtime/webcore/fetch/FetchTasklet.rs` near `Response::init` for the
318+canonical example of this bug class and its fix.
232319
233−**GOOD, do this:**
320+## Common Patterns
234321
235−```ts
236−import { readFile } from "node:fs";
237−test("foo", async () => {
238− expect(await readFile("ok.txt")).toBe("");
239−});
240−```
322+```rust
323+// Read a file, return JS error on failure
324+let contents = match bun_sys::File::openat(Fd::cwd(), path, O::RDONLY, 0)
325+ .and_then(|f| f.read_to_end())
326+{
327+ Ok(bytes) => bytes,
328+ Err(err) => return Ok(err.to_js(global)?),
329+};
241330
242−### Test Utilities
331+// Heap-allocated FFI handle with explicit lifecycle
332+let raw = bun_core::heap::into_raw(Box::new(MyHandle::new()));
333+register_with_c(raw);
334+// ... later, in the matching teardown callback:
335+unsafe { bun_core::heap::destroy(raw) };
243336
244−- Use `describe.each()` for parameterized tests
245−- Use `toMatchSnapshot()` for snapshot testing
246−- Use `beforeAll()`, `afterEach()`, `beforeEach()` for setup/teardown
247−- Track resources (servers, clients) in arrays for cleanup in `afterEach()`
337+// Hashing
338+bun_wyhash::hash(bytes) // u64
339+bun_wyhash::hash_with_seed(seed, bytes)
340+```
248341
