Skip to content

Fix: Bun Not Working — Node.js Module Incompatible, Native Addon Fails, or bun test Errors

FixDevs ·

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 Bun

Or a package that works with Node doesn’t work with Bun:

bun run app.ts
# TypeError: dns.promises.lookup is not a function

Or bun test fails with tests that pass in Jest:

bun test
# error: Cannot use 'jest.mock()' — use 'mock.module()' instead

Or Bun.serve behaves differently than expected:

const server = Bun.serve({
  port: 3000,
  fetch(req) {
    return new Response('Hello');
  },
});
// Works, but WebSocket upgrade isn't handled

Why This Happens

Bun is not a drop-in replacement for Node.js in all cases:

  • Native addons (.node files) are not supported — packages like bcrypt, sharp, canvas, and sqlite3 that use node-gyp to 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 test is Jest-compatible but not identical — Bun’s test runner supports most Jest APIs but some mocking APIs differ. jest.mock() doesn’t exist; use mock.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 recommendations

Fix 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 installed

Fix 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-is

Performance 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 server

Still 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles