process.getBuiltinModule()

Loads a Node.js built-in module by id without using require() or import.

Signature
process.getBuiltinModule(id)
  • Added in Node.js v22.3.0

Parameters

ParameterTypeDescription
$idstringID of the built-in module being requested, such as 'fs' or 'node:path'. Accepts any id recognised by module.isBuiltin(id); some built-ins require the node: prefix.

Return value

Object | undefinedThe exports object of the built-in module, or undefined if id does not name a built-in available in this process.

Errors

Does not throw for an unknown id — it returns undefined instead. Throws ERR_INVALID_ARG_TYPE if id is not a string.

Changelog

  • PHP v20.16.0Backported to the v20 line, so a feature check rather than a version check is the reliable way to detect support.

process.getBuiltinModule() is a globally reachable function that returns a core module's exports. It exists to solve one specific problem: a library shipped as ESM that must run in Node.js, a browser, and an edge runtime cannot write import fs from 'node:fs', because the bare specifier fails to resolve everywhere else — at parse time, before any runtime check can guard it.

Why not dynamic import()

await import('node:fs') works, but it is asynchronous. That either turns your module into a top-level-await module or forces a synchronous API to become a promise. getBuiltinModule() is synchronous and can sit inside a plain if.

What trips people up

  • An unknown id returns undefined rather than throwing, so a typo produces Cannot read properties of undefined far from the cause.
  • Some built-ins are only reachable with the prefix: getBuiltinModule('test') is undefined, getBuiltinModule('node:test') is not.
  • It returns the genuine built-in even when require.cache has been patched, which makes mocking libraries unable to intercept it.
  • It loads built-ins only. For userland packages, get node:module from it and call createRequire().

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Let an ESM library read a local file in Node.js while the same bundle runs unchanged in a browser.
  • Reach node:module inside ESM to build a createRequire() for a CommonJS-only dependency.
  • Bypass a patched require.cache in a test helper that must see the real fs, not the mock.

Frequently asked questions

What is the difference between process.getBuiltinModule and require?

getBuiltinModule loads only built-in modules, is available as a global function in both CommonJS and ESM, and always returns the genuine built-in even if require.cache has been patched. require additionally resolves userland packages and relative paths, which getBuiltinModule cannot do.

Why does process.getBuiltinModule return undefined?

Either the id is not a built-in — userland package names always return undefined — or the module requires the node: prefix, as 'node:test' does. Check with module.isBuiltin(id) and prefer prefixed ids everywhere.

Which Node.js version added process.getBuiltinModule?

It landed in v22.3.0 and was backported to v20.16.0, so a version-number comparison is unreliable. Feature-detect with typeof process.getBuiltinModule === 'function' instead.

More examples

One ESM bundle for Node.js and the browser

Writing `import fs from 'node:fs'` here would break the browser build at resolution time, before any runtime check could run, and `await import('node:fs')` would force readCache to become async. The optional chaining covers three cases at once: no process object, an older Node.js without the method, and a non-Node runtime.

Example · javascript
// cache.mjs - imported unchanged by a bundler targeting the browser.

const fs = globalThis.process?.getBuiltinModule?.('node:fs');

export function readCache(key) {
  if (fs) {
    // Node.js: use the real filesystem.
    try {
      return JSON.parse(fs.readFileSync(`.cache/${key}.json`, 'utf8'));
    } catch {
      return null;
    }
  }

  // Browser or edge runtime: fall back to localStorage.
  const raw = globalThis.localStorage?.getItem(key);
  return raw ? JSON.parse(raw) : null;
}

console.log('cache backend:', fs ? 'filesystem' : 'localStorage');

Reaching a CommonJS dependency from ESM

This is the pattern the official documentation shows. getBuiltinModule cannot load userland packages, so the two-step is required: fetch node:module with it, then build a require() bound to this file's URL. The explicit throw turns an otherwise cryptic 'cannot read createRequire of undefined' into a clear version message.

Example · javascript
// plugin-loader.mjs

const module_ = process.getBuiltinModule('node:module');
if (!module_) {
  throw new Error('this loader requires Node.js 20.16 or 22.3 and above');
}

const require = module_.createRequire(import.meta.url);

// Now a CommonJS-only package can be loaded synchronously from ESM.
const legacyPlugin = require('./plugins/legacy.cjs');
console.log('loaded plugin:', legacyPlugin.name);

// Built-ins still come from getBuiltinModule; only userland goes through require.
const os = process.getBuiltinModule('node:os');
console.log('platform:', os.platform());

Immune to a patched require cache

References returned by getBuiltinModule always point at the genuine built-in, whatever has been done to require.cache. That is useful for a reporter that must write real files while the suite is mocked, and it is also the reason mocking libraries built on cache patching cannot intercept code that uses this function.

Example · javascript
// A test helper has stubbed fs for the code under test.
require.cache[require.resolve('fs')] = {
  id: 'fs',
  loaded: true,
  exports: { readFileSync: () => 'STUBBED' },
};

console.log('require gives:', require('fs').readFileSync('x'));

const realFs = process.getBuiltinModule('fs');
console.log('getBuiltinModule bypasses the stub:',
  realFs.readFileSync !== require('fs').readFileSync);

// Output:
// require gives: STUBBED
// getBuiltinModule bypasses the stub: true

Discussion

  • Be the first to comment on this lesson.