Fix: Bun Not Working — Node.js Module Incompatible, Native Addon Fails, or bun test Errors
Quick Answer
How to fix Bun runtime issues — Node.js API compatibility, native addons (node-gyp), Bun.serve vs Node http, bun test differences from Jest, and common package incompatibilities.
The Problem
A Node.js package fails to load in Bun:
bun run server.ts
# error: Cannot find module 'bcrypt'
# Hint: native Node.js addons (node-gyp) are not supported in BunOr a package that works with Node doesn’t work with Bun:
bun run app.ts
# TypeError: dns.promises.lookup is not a functionOr bun test fails with tests that pass in Jest:
bun test
# error: Cannot use 'jest.mock()' — use 'mock.module()' insteadOr Bun.serve behaves differently than expected:
const server = Bun.serve({
port: 3000,
fetch(req) {
return new Response('Hello');
},
});
// Works, but WebSocket upgrade isn't handledWhy This Happens
Bun is not a drop-in replacement for Node.js in all cases:
- Native addons (
.nodefiles) are not supported — packages likebcrypt,sharp,canvas, andsqlite3that usenode-gypto compile C++ extensions don’t work in Bun. Use pure-JavaScript or Bun-native alternatives. - Bun implements the Node.js API, not all of it — Bun tracks Node.js compatibility but some APIs are missing or behave differently. The Bun Node.js compatibility page documents what’s supported.
bun testis Jest-compatible but not identical — Bun’s test runner supports most Jest APIs but some mocking APIs differ.jest.mock()doesn’t exist; usemock.module().- Module resolution differences — Bun supports both CommonJS and ESM, but the resolution algorithm differs slightly from Node.js in edge cases.
Fix 1: Replace Native Addons with Pure-JS Alternatives
Native addons compiled with node-gyp don’t run in Bun. Use these alternatives:
# bcrypt → bcryptjs (pure JavaScript)
bun remove bcrypt
bun add bcryptjs
# Usage is identical
import bcrypt from 'bcryptjs';
const hash = await bcrypt.hash('password', 10);
const valid = await bcrypt.compare('password', hash);# sharp (image processing) — actually works in Bun via the libvips bindings
# Try it: bun add sharp
# If it fails, use @squoosh/lib or jimp
bun add jimp # Pure JS image processing
# canvas → use Bun's built-in canvas (Bun 1.1+) or @napi-rs/canvas
# @napi-rs/canvas supports Bun natively
bun add @napi-rs/canvas
# sqlite3 → use bun:sqlite (built-in, much faster)
import { Database } from 'bun:sqlite';
const db = new Database('mydb.sqlite');
db.run('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');Check if a package supports Bun:
# Try installing and running — Bun will warn about native modules
bun add package-name
bun run -e "import 'package-name'"
# Or check the package's GitHub for Bun compatibility notes
# https://bun.sh/guides has Bun-specific recommendationsFix 2: Handle Node.js API Differences
Most Node.js APIs work in Bun, but some have gaps:
// Check Bun version and Node.js compat
console.log(Bun.version); // e.g., "1.1.38"
console.log(process.version); // Node.js compatibility version
// WORKS in Bun — common Node.js APIs
import fs from 'fs/promises';
import path from 'path';
import crypto from 'crypto';
import { EventEmitter } from 'events';
import { Readable, Writable } from 'stream';
// ALSO WORKS — Bun implements these
import { createServer } from 'http';
import { createServer as createHttpsServer } from 'https';
import { Worker } from 'worker_threads';
import { createHash, randomBytes } from 'crypto';
// Bun-NATIVE alternatives (faster than Node.js equivalents)
// File I/O
const file = Bun.file('./data.json');
const text = await file.text();
const json = await file.json();
await Bun.write('./output.json', JSON.stringify(data));
// Hashing
const hash = Bun.CryptoHasher.hash('sha256', 'hello world', 'hex');
// Passwords
const hashed = await Bun.password.hash('mypassword');
const valid = await Bun.password.verify('mypassword', hashed);
// Uses argon2id by default (more secure than bcrypt)Environment variables:
// Both work in Bun
process.env.MY_VAR // Node.js style
Bun.env.MY_VAR // Bun style — same values
// Bun auto-loads .env files — no dotenv needed!
// .env, .env.local, .env.production, .env.development
// (based on NODE_ENV value)Fix 3: Use Bun.serve for HTTP
Bun.serve is Bun’s built-in HTTP server — significantly faster than Node’s http.createServer:
// Basic HTTP server
const server = Bun.serve({
port: 3000,
hostname: '0.0.0.0', // Listen on all interfaces
async fetch(req: Request) {
const url = new URL(req.url);
if (url.pathname === '/') {
return new Response('Hello, Bun!');
}
if (url.pathname === '/api/users') {
const users = await db.query.users.findMany();
return Response.json(users);
}
return new Response('Not Found', { status: 404 });
},
error(error: Error) {
return new Response(`Internal error: ${error.message}`, { status: 500 });
},
});
console.log(`Listening on http://localhost:${server.port}`);
// WebSocket support
const wsServer = Bun.serve<{ userId: string }>({
port: 3001,
fetch(req, server) {
const token = req.headers.get('authorization');
const userId = verifyToken(token);
if (!userId) return new Response('Unauthorized', { status: 401 });
// Upgrade to WebSocket
const upgraded = server.upgrade(req, { data: { userId } });
if (upgraded) return; // undefined on successful upgrade
return new Response('Expected WebSocket', { status: 426 });
},
websocket: {
open(ws) {
console.log(`User ${ws.data.userId} connected`);
ws.subscribe('broadcast'); // Pub/sub channel
},
message(ws, message) {
ws.publish('broadcast', `${ws.data.userId}: ${message}`);
},
close(ws, code, reason) {
console.log(`User ${ws.data.userId} disconnected`);
},
},
});Use Elysia or Hono for routing:
// Elysia — designed for Bun (TypeScript-first, very fast)
import { Elysia, t } from 'elysia';
const app = new Elysia()
.get('/', () => 'Hello Elysia')
.post('/sign-in', ({ body }) => signIn(body), {
body: t.Object({
email: t.String({ format: 'email' }),
password: t.String({ minLength: 8 }),
}),
})
.listen(3000);
// Hono — works on Bun, Cloudflare Workers, Deno, Node.js
import { Hono } from 'hono';
const app = new Hono();
app.get('/', (c) => c.text('Hello Hono'));
app.post('/users', async (c) => {
const body = await c.req.json();
return c.json({ created: body });
});
export default {
port: 3000,
fetch: app.fetch,
};Fix 4: Fix bun test Differences from Jest
Bun’s test runner is mostly Jest-compatible but has differences:
// bun test — differences from Jest
import { describe, test, expect, mock, beforeEach } from 'bun:test';
// WRONG — jest.mock doesn't exist in bun
jest.mock('./module'); // ReferenceError: jest is not defined
// CORRECT — use mock.module
mock.module('./module', () => ({
fetchData: async () => ({ id: 1, name: 'Alice' }),
}));
// jest.fn() equivalent
const mockFn = mock(() => 'mocked value');
mockFn.mockReturnValue('new value'); // Same as jest.fn().mockReturnValue
mockFn.mockResolvedValue('async value'); // Same as jest.fn().mockResolvedValue
console.log(mockFn.mock.calls); // Access call history
// jest.spyOn equivalent
import * as fs from 'fs/promises';
const spy = jest.spyOn(fs, 'readFile'); // DOESN'T EXIST in bun
// Instead:
const originalReadFile = fs.readFile;
fs.readFile = mock(() => 'mocked content');
// Restore:
fs.readFile = originalReadFile;bun test configuration:
// bunfig.toml — Bun's config file
[test]
timeout = 10000 # Default test timeout (ms)
coverage = true # Enable coverage
coverageThreshold = 80 # Minimum coverage %// Timer mocks
import { describe, test, expect, jest } from 'bun:test';
test('timer test', () => {
jest.useFakeTimers(); // Bun supports this
const fn = mock();
setTimeout(fn, 1000);
jest.runAllTimers();
expect(fn).toHaveBeenCalledTimes(1);
jest.useRealTimers();
});Fix 5: Bundling with Bun
Bun has a built-in bundler that replaces esbuild/webpack for many use cases:
// Bundle for production
const result = await Bun.build({
entrypoints: ['./src/index.ts'],
outdir: './dist',
target: 'bun', // 'bun' | 'node' | 'browser'
format: 'esm', // 'esm' | 'cjs' | 'iife'
minify: true,
sourcemap: 'external',
splitting: true, // Code splitting
define: {
'process.env.NODE_ENV': '"production"',
},
});
if (!result.success) {
console.error(result.logs);
process.exit(1);
}# CLI bundling
bun build ./src/index.ts --outdir ./dist --target bun --minify
# Bundle for browser
bun build ./src/client.ts --outdir ./public --target browser --splitting
# Bundle as a single executable
bun build ./src/index.ts --compile --outfile my-app
./my-app # Runs without Bun installedFix 6: Migrate a Node.js Project to Bun
Step-by-step migration:
# 1. Install Bun
curl -fsSL https://bun.sh/install | bash
# 2. Install dependencies with Bun (reads package.json)
bun install # ~25x faster than npm
# 3. Run the app with Bun
bun run src/index.ts # Bun runs TypeScript natively — no tsc step!
# 4. Replace scripts in package.json
# Before: "dev": "ts-node src/index.ts"
# After: "dev": "bun run src/index.ts"
# Before: "build": "tsc"
# After: "build": "bun build src/index.ts --outdir dist"
# 5. Handle native module failures
# Replace: bcrypt → bcryptjs, sqlite3 → bun:sqlite, etc.
# 6. Migrate dotenv (not needed with Bun)
# Remove: import 'dotenv/config'
# Bun loads .env automatically
# 7. Run tests
bun test # Jest-compatible — most tests work as-isPerformance comparison script:
# Check if Bun is faster for your specific workload
time node dist/index.js # Node.js startup
time bun run src/index.ts # Bun startup (TypeScript, no build step)
# HTTP benchmark
npx autocannon http://localhost:3000 # Against Node.js server
npx autocannon http://localhost:3001 # Against Bun.serve serverStill Not Working?
Bun.serve returns 500 for all routes — check the error handler. By default, Bun catches errors in fetch() and logs them, but if you have an error handler that doesn’t return a Response, the request hangs. Always return a Response from both fetch and error.
ESM/CJS interop issues — Bun handles both ESM and CommonJS, but mixed-module packages can cause issues. If you see Cannot use import statement in a CommonJS module, add "type": "module" to your package.json, or rename files to .mts/.mjs. For the opposite error, use createRequire from the module package.
Bun process exits immediately — unlike Node.js, Bun doesn’t keep the process alive if there’s no pending I/O or timers. If your app exits right after starting, ensure you have a persistent listener (e.g., Bun.serve, an interval, or an open database connection) to keep the event loop running.
Package version conflicts with Bun’s lockfile — Bun uses bun.lockb (binary format) instead of package-lock.json. If you switch between npm install and bun install, you may get version mismatches. Stick to one package manager and delete the other’s lockfile.
For related JavaScript runtime issues, see Fix: Node Uncaught Exception and Fix: Node.js Stream Error.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Node.js Stream Error — Pipe Not Working, Backpressure, or Premature Close
How to fix Node.js stream issues — pipe and pipeline errors, backpressure handling, Transform streams, async iteration, error propagation, and common stream anti-patterns.
Fix: Node.js UnhandledPromiseRejection and uncaughtException — Crashing Server
How to handle Node.js uncaughtException and unhandledRejection events — graceful shutdown, error logging, async error boundaries, and keeping servers alive safely.
Fix: Socket.IO CORS Error — Cross-Origin Connection Blocked
How to fix Socket.IO CORS errors — server-side CORS configuration, credential handling, polling vs WebSocket transport, proxy setup, and common connection failures.