esbuild 06 ⚡ The platform Setting — Browser vs. Node resolution rules
esbuild’s platform option is not a single flag; it is a bundle of resolution rules that change how the bundler interprets imports, externalizes built-ins, and selects conditional exports from package.json. Setting platform: 'browser' tells esbuild that the output will run in a browser, so Node built-ins must be externalized or replaced, and the browser condition in package exports is active. Setting platform: 'node' tells esbuild that the output will run in Node, so built-in modules like fs and path are automatically treated as external and the node condition is active. The default is browser, which means a build that forgets to set platform: 'node' will fail on a require('fs') with an error about the module not being available in the browser .
Key point: platform determines both the default resolution conditions (browser or node) and the automatic externalization of Node built-ins. It does not change the output format; format is a separate option.
Why platform changes resolution
The built-in externalization problem. A Node script imports fs and path. A browser bundle cannot include those modules because the browser has no filesystem. When platform: 'node', esbuild marks fs, path, crypto, and the other Node built-ins as external, so the import is preserved in the output and resolved by Node at runtime . When platform: 'browser' (the default), those imports are not externalized, and esbuild attempts to resolve them as regular packages, which fails unless a browser-compatible shim is installed.
The conditional exports problem. A package can export different files for different environments using the exports field in package.json. A package might export a Node-specific entry under the node condition and a browser-specific entry under the browser condition. The platform option sets the default condition that esbuild uses when resolving that package . A build with platform: 'browser' picks the browser entry; a build with platform: 'node' picks the Node entry.
The neutral platform. The neutral value is for environments that are neither a browser nor Node, such as a Cloudflare Worker or a Deno edge function. It does not set the browser or node condition, and it does not externalize Node built-ins automatically. The developer must configure the conditions and external modules explicitly .
a. The browser platform and its consequences
The browser platform is the default. It sets the browser condition for package resolution and does not externalize Node built-ins. A direct require('fs') in a browser build fails at bundle time because esbuild cannot find a package named fs in node_modules.
The common pattern for handling a Node-only dependency in code that also targets the browser is a try/catch around the require. esbuild treats the contents of a try/catch block as optional, so it does not fail the build when the require cannot be resolved . The code checks at runtime whether the module is available.
let NodeCrypto
try {
NodeCrypto = require('crypto')
} catch {
// Browser fallback
}
The alternative is the browser field in package.json, which maps a Node module to false or to a browser-compatible shim. esbuild respects this field, so a package that lists "crypto": false in its browser field will not attempt to bundle crypto when the platform is browser .
b. The node platform and its consequences
The node platform sets the node condition and externalizes every module in esbuild’s list of Node built-ins. An import of fs, path, crypto, worker_threads, or node:fs is preserved in the output and resolved by Node at runtime. The node: prefix is also recognized as a built-in marker .
The node platform is the correct choice for a server-side bundle, a CLI tool, or a test runner. The output can still be bundled and minified, but the Node built-ins are left as external references. The format option determines whether the output uses require (CJS) or import (ESM), and the node platform does not change that choice.
c. Conditions and the neutral platform
The conditions option adds extra conditions beyond the platform default. A package that uses a custom condition like "development" or "react-server" can be resolved by adding that condition to the list.
esbuild.build({
platform: 'browser',
conditions: ['development'],
})
The neutral platform is for environments where neither the browser nor Node is the correct model. It sets no default condition and externalizes no built-ins. The developer must specify the conditions and the external modules. This is appropriate for edge runtimes, where some Web APIs are available but Node built-ins are not .
Complete Example Session
// ============================================
// PART 1: THE DEFAULT (BROWSER)
// ============================================
esbuild.build({
entryPoints: ['app.js'],
bundle: true,
outfile: 'out.js',
})
// platform defaults to 'browser'.
// The 'browser' condition is active.
// Node built-ins are NOT externalized.
// ============================================
// PART 2: A NODE BUILT-IN FAILS ON BROWSER
// ============================================
// app.js
import fs from 'fs'
esbuild.build({ entryPoints: ['app.js'], bundle: true })
// Error: Could not resolve "fs"
// The browser platform does not externalize Node built-ins.
// ============================================
// PART 3: THE NODE PLATFORM
// ============================================
esbuild.build({
entryPoints: ['app.js'],
bundle: true,
platform: 'node',
outfile: 'out.js',
})
// The 'node' condition is active.
// 'fs' is externalized and resolved by Node at runtime.
// ============================================
// PART 4: THE node: PREFIX
// ============================================
import fs from 'node:fs'
// With platform: 'node', the node: prefix is recognized.
// The module is externalized.
// ============================================
// PART 5: CONDITIONAL EXPORTS
// ============================================
// package.json of a dependency:
{
"exports": {
".": {
"browser": "./browser.js",
"node": "./node.js",
"default": "./index.js"
}
}
}
// platform: 'browser' → browser.js
// platform: 'node' → node.js
// ============================================
// PART 6: THE NEUTRAL PLATFORM
// ============================================
esbuild.build({
platform: 'neutral',
conditions: ['worker'],
})
// No default condition.
// No automatic externalization.
// The developer configures both.
// ============================================
// PART 7: CUSTOM CONDITIONS
// ============================================
esbuild.build({
platform: 'browser',
conditions: ['react-server'],
})
// The browser condition plus the custom condition.
// ============================================
// PART 8: THE BROWSER FIELD
// ============================================
// package.json:
{
"browser": {
"crypto": false
}
}
// With platform: 'browser', 'crypto' resolves to false.
// esbuild does not attempt to bundle it.
// ============================================
// PART 9: THE TRY/CATCH PATTERN
// ============================================
let crypto
try {
crypto = require('crypto')
} catch {
// Browser fallback
}
// esbuild ignores the unresolved require inside try/catch.
// The build succeeds for both platforms.
// ============================================
// PART 10: THE PLATFORM MATRIX
// ============================================
// platform: 'browser'
// condition: browser
// built-ins: not externalized
//
// platform: 'node'
// condition: node
// built-ins: externalized
//
// platform: 'neutral'
// condition: none
// built-ins: not externalized
The ten parts covered the default browser platform, the built-in failure, the node platform, the node: prefix, conditional exports, the neutral platform, custom conditions, the browser field, the try/catch pattern, and the platform matrix.
Quick Reference
Platform values
| Value | Default condition | Node built-ins | Use case |
|---|---|---|---|
browser (default) | browser | Not externalized | Web apps |
node | node | Externalized | Server, CLI |
neutral | None | Not externalized | Edge, custom |
Platform consequences
| Behavior | browser | node | neutral |
|---|---|---|---|
browser condition | Active | Inactive | Inactive |
node condition | Inactive | Active | Inactive |
fs externalized | No | Yes | No |
node:fs externalized | No | Yes | No |
browser field respected | Yes | No | No |
Related options
| Option | Purpose |
|---|---|
conditions | Additional resolution conditions |
external | Manual externalization |
format | Output format (independent of platform) |
target | Syntax lowering (independent of platform) |
Best Practices
✅ Do This:
// Set platform explicitly for Node builds
platform: 'node' // ✅
// Use try/catch for optional Node dependencies
try { require('crypto') } catch {} // ✅
// Use the node: prefix for built-ins
import fs from 'node:fs' // ✅
// Set neutral for edge environments
platform: 'neutral', conditions: ['worker'] // ✅
❌ Don’t Do This:
// Rely on the default for a Node build
esbuild.build({ bundle: true }) // platform is browser // ❌
// Import a Node built-in without platform: 'node'
import fs from 'fs' // fails on browser // ❌
// Assume neutral externalizes built-ins
platform: 'neutral' // no externalization // ❌
// Use platform to change the output format
// format is a separate option // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
Could not resolve "fs" | Default platform is browser | Set platform: 'node' |
| Browser build bundles a Node shim | No browser field or try/catch | Add the browser field or wrap in try/catch |
| Wrong conditional export chosen | Platform not set | Set platform explicitly |
| Neutral build fails on a built-in | Neutral does not externalize | Add external: ['fs'] |
| Platform does not affect syntax | target controls lowering | Set target separately |
Real-World Examples
1. Browser bundle
esbuild.build({ platform: 'browser', bundle: true })
2. Node bundle
esbuild.build({ platform: 'node', bundle: true })
3. Neutral edge bundle
esbuild.build({ platform: 'neutral', conditions: ['worker'] })
4. Optional Node dependency
try { require('crypto') } catch {}
5. Browser field
{ "browser": { "crypto": false } }
6. Custom condition
esbuild.build({ platform: 'browser', conditions: ['react-server'] })
7. node: prefix
import fs from 'node:fs'
8. External a built-in manually
esbuild.build({ external: ['fs'] })
9. Conditional exports
{ "exports": { ".": { "browser": "./b.js", "node": "./n.js" } } }
10. Platform matrix
browser: browser condition, no externalization
node: node condition, externalization
neutral: no condition, no externalization
Visual
The resolution pipeline
┌─────────────────────────────────────────────────────────────┐
│ PLATFORM → RESOLUTION │
│ │
│ platform: 'node' │
│ ├── conditions: ['node'] │
│ ├── built-in import → external │
│ └── package exports → node entry │
│ │
│ platform: 'browser' │
│ ├── conditions: ['browser'] │
│ ├── built-in import → error (unless shimmed) │
│ └── package exports → browser entry │
│ │
│ platform: 'neutral' │
│ ├── conditions: [] │
│ ├── built-in import → error │
│ └── package exports → default entry │
└─────────────────────────────────────────────────────────────┘
Built-in externalization
┌─────────────────────────────────────────────────────────────┐
│ import fs from 'fs' │
│ │
│ platform: 'node' │
│ → fs is in BuiltInNodeModules │
│ → marked external │
│ → output: import fs from 'fs' │
│ │
│ platform: 'browser' │
│ → fs is not externalized │
│ → resolver looks for node_modules/fs │
│ → not found → build error │
└─────────────────────────────────────────────────────────────┘
Conditional exports
┌─────────────────────────────────────────────────────────────┐
│ package.json exports │
│ │
│ { │
│ "exports": { │
│ ".": { │
│ "browser": "./browser.js", │
│ "node": "./node.js", │
│ "default": "./index.js" │
│ } │
│ } │
│ } │
│ │
│ platform: 'browser' → browser.js │
│ platform: 'node' → node.js │
│ platform: 'neutral' → index.js │
└─────────────────────────────────────────────────────────────┘
The try/catch escape
┌─────────────────────────────────────────────────────────────┐
│ try { require('crypto') } catch {} │
│ │
│ platform: 'browser' │
│ → esbuild sees the require inside try/catch │
│ → treats it as optional │
│ → does not fail the build │
│ → runtime: throws, caught by the catch │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Default platform | browser |
| Values | browser, node, neutral |
node effect | Sets node condition, externalizes built-ins |
browser effect | Sets browser condition, no externalization |
neutral effect | No condition, no externalization |
| Conditional exports | Controlled by platform condition |
| Built-in externalization | Automatic only on node |
| Optional dependency pattern | try { require(...) } catch {} |
Key takeaways:
platformchanges resolution rules, not output format. It sets the default condition for package exports and controls whether Node built-ins are externalized. Theformatoption is separate.- The default is
browser. A build that importsfswithout settingplatform: 'node'fails at bundle time because the browser platform does not externalize Node built-ins . platform: 'node'externalizes every Node built-in.fs,path,crypto,worker_threads, and thenode:prefixed variants are preserved in the output and resolved by Node at runtime .platform: 'neutral'is for edge environments. It sets no condition and externalizes nothing. The developer must configureconditionsandexternalexplicitly .- The
browserfield inpackage.jsonmaps modules tofalseor shims. esbuild respects this field, so a package that markscryptoasfalsewill not attempt to bundle it on the browser platform . try/catcharound a require makes it optional. esbuild does not fail the build when a require inside atryblock cannot be resolved, which is the standard pattern for a dependency that is Node-only but the code also runs in the browser .
Remember: The platform option is the boundary between the Node module graph and the browser module graph. A Node build externalizes built-ins and resolves the node condition. A browser build resolves the browser condition and attempts to bundle everything, which fails on built-ins unless the package provides a browser shim or the code wraps the import in try/catch. Set the platform explicitly, because the default is browser, and a server bundle that forgets the setting will fail on the first require('fs').
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!