process.getBuiltinModule()
Loads a Node.js built-in module by id without using require() or import.
process.getBuiltinModule(id)- Added in Node.js v22.3.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$id | string | ID 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
undefinedrather than throwing, so a typo producesCannot read properties of undefinedfar from the cause. - Some built-ins are only reachable with the prefix:
getBuiltinModule('test')isundefined,getBuiltinModule('node:test')is not. - It returns the genuine built-in even when
require.cachehas been patched, which makes mocking libraries unable to intercept it. - It loads built-ins only. For userland packages, get
node:modulefrom it and callcreateRequire().
Example
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.
// 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.
// 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.
// 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