| | |

esbuild 06 ⚡ The platform Setting — Browser vs. Node resolution rules

⏱️ Estimated read: 9 min | |

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

ValueDefault conditionNode built-insUse case
browser (default)browserNot externalizedWeb apps
nodenodeExternalizedServer, CLI
neutralNoneNot externalizedEdge, custom

Platform consequences

Behaviorbrowsernodeneutral
browser conditionActiveInactiveInactive
node conditionInactiveActiveInactive
fs externalizedNoYesNo
node:fs externalizedNoYesNo
browser field respectedYesNoNo

Related options

OptionPurpose
conditionsAdditional resolution conditions
externalManual externalization
formatOutput format (independent of platform)
targetSyntax 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

PitfallWhy It HappensFix
Could not resolve "fs"Default platform is browserSet platform: 'node'
Browser build bundles a Node shimNo browser field or try/catchAdd the browser field or wrap in try/catch
Wrong conditional export chosenPlatform not setSet platform explicitly
Neutral build fails on a built-inNeutral does not externalizeAdd external: ['fs']
Platform does not affect syntaxtarget controls loweringSet 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

ItemValue
Default platformbrowser
Valuesbrowser, node, neutral
node effectSets node condition, externalizes built-ins
browser effectSets browser condition, no externalization
neutral effectNo condition, no externalization
Conditional exportsControlled by platform condition
Built-in externalizationAutomatic only on node
Optional dependency patterntry { require(...) } catch {}

Key takeaways:

  • platform changes resolution rules, not output format. It sets the default condition for package exports and controls whether Node built-ins are externalized. The format option is separate.
  • The default is browser. A build that imports fs without setting platform: '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 the node: 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 configure conditions and external explicitly .
  • The browser field in package.json maps modules to false or shims. esbuild respects this field, so a package that marks crypto as false will not attempt to bundle it on the browser platform .
  • try/catch around a require makes it optional. esbuild does not fail the build when a require inside a try block 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').

Was this helpful?


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!