nodejs/node
Patterns and conventions
Node.js core has accumulated a strong house style over the last 15+ years. This page captures the conventions you have to internalize before sending a patch — both for the JavaScript side under lib/ and the C++ side under src/. It is the single most useful page for first-time contributors after Getting started.
The primordials rule
User code can replace anything on a built-in prototype. If lib/ calls arr.map(fn) and the user has done Array.prototype.map = …, core breaks. To prevent that, every internal JS file pulls primordials from a frozen object that was captured before user code ran:
const { ArrayPrototypeMap, ObjectKeys, StringPrototypeStartsWith } =
primordials;The bindings are built in lib/internal/per_context/primordials.js, which captures and "uncurries" the prototype methods using Function.prototype.bind so that mutations after bootstrap have no effect.
Rules of thumb:
- Inside
lib/internal/**, never callarr.map,obj.hasOwnProperty,str.startsWith, etc. directly. UseArrayPrototypeMap(arr, fn),ObjectPrototypeHasOwnProperty(obj, k),StringPrototypeStartsWith(str, prefix). - Use
SafeMap,SafeSet,SafeWeakMap,SafeFinalizationRegistry,SafeArrayIteratorinstead ofnew Map(), etc., when you need iteration that does not depend on a possibly-replacedSymbol.iterator. - The exception is
lib/internal/per_context/primordials.jsitself, which uses/* eslint-disable node-core/prefer-primordials */because it is doing the capturing. - Tests in
test/and benchmarks inbenchmark/do not have to use primordials — they are user-side code.
A custom ESLint rule node-core/prefer-primordials (configured in tools/eslint-rules/) catches violations. make lint-js will reject a PR that adds arr.map-style calls in core.
Because primordials add a function call, hot paths in core sometimes inline the access (with a comment justifying it). The rule of thumb in lib/internal/per_context/primordials.js itself is: "Use of primordials have sometimes a dramatic impact on performance, please benchmark all changes made in performance-sensitive areas of the codebase."
Error handling: internal/errors.js
Every error thrown by core is registered in lib/internal/errors.js with a stable code (e.g. ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE). Codes are documented in doc/api/errors.md and have backwards-compatibility guarantees.
Pattern:
const {
codes: { ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE },
} = require('internal/errors');
if (typeof x !== 'string') {
throw new ERR_INVALID_ARG_TYPE('x', 'string', x);
}Adding a new error code requires documenting it in doc/api/errors.md and (almost always) writing a test. C++ errors live in src/node_errors.{cc,h} and are surfaced to JS via the same code names through node_errors.cc.
Argument validation: internal/validators.js
Validation helpers live in lib/internal/validators.js: validateString, validateInteger, validateNumber, validateArray, validateAbortSignal, validateBuffer, validateFunction, etc. Use them. They are tuned to throw ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE with consistent messages and matching behaviour across the API.
Module shape
- Public modules in
lib/*.jsare thin re-export shells that pull fromlib/internal/<name>.js(or from a directory likelib/internal/<name>/). - New built-in JS code lives under
lib/internal/, never the top level. - All built-in
.jsfiles start with'use strict';and use single-quoted strings. - The
__proto__: nullidiom is used liberally on options-bag objects to avoid prototype-pollution risks. - CommonJS modules export through
module.exports = { … }at the bottom; ESM-internal files are rare. - The custom builtin loader (
BuiltinModuleinlib/internal/bootstrap/realm.js) requires thatrequire('internal/foo')strings appear as static literals so the dependency graph is statically analyzable.
Snapshot-friendliness
Anything in lib/internal/bootstrap/** runs while building the V8 startup snapshot. That snapshot is captured to a binary blob and replayed on every Node startup. Consequences:
- No
process.env,process.argv, file-system access, or random-numbers inlib/internal/bootstrap/**. Read those fromlib/internal/process/pre_execution.jsinstead, which runs after the snapshot is deserialized. - No async calls. Bootstrap is synchronous.
- Every binding fetched at bootstrap time has to be deterministic. Lazy loading via
lib/internal/util.js#deferredAccessorsis the workaround.
lib/internal/bootstrap/realm.js and lib/internal/bootstrap/node.js document this contract in their headers.
C++ conventions
The C++ tree under src/ follows a tight house style:
-inl.hheaders hold inline implementations.foo.hdeclares;foo-inl.hdefines inline;foo.ccdefines non-inline. Including patterns matter:foo.ccincludesfoo-inl.h; users offooinclude onlyfoo.h.- Use
node::Environment*/node::Realm*rather than capturingv8::Isolate*directly. The rightEnvironmentfor an isolate isEnvironment::GetCurrent(args). - Wrap libuv handles in
HandleWrapsubclasses; one-shot requests inReqWrap. Anything that needs a JS callback inherits fromAsyncWrap. See primitives › async-wrap-and-handle-wrap. - Use
BaseObjectPtr<T>(a strong reference) andBaseObjectWeakPtr<T>(a weak reference) instead of raw pointers across async boundaries. - Bindings are registered with
NODE_BINDING_CONTEXT_AWARE_INTERNAL(<name>, Initialize)(seesrc/node_binding.cc). Initialization functions install JS-visible function templates; per-realm storage lives in aBindingDatastruct. - Externally-visible C APIs go through
src/node.h,src/js_native_api.h, orsrc/node_api.h. Anything else is internal and may change. MaybeStackBuffer<T>(src/util.h) is used everywhere for short buffers to avoid heap allocation in the common case.- OpenSSL access goes through
deps/ncrypto/, not direct OpenSSL calls. See systems › crypto-and-tls. - C++ code is formatted with
clang-format(.clang-format) and linted with a forkedcpplint(tools/cpplint.py, configured in.cpplint).make format-cppandmake lint-cppare the canonical commands.
Testing conventions
- Most tests live in
test/parallel/test-<topic>-<scenario>.jsand run in parallel. - Tests that mutate shared state, listen on fixed ports, or are timing-sensitive go in
test/sequential/. - Long-running tests live in
test/pummel/; very-rare flaky tests intest/known_issues/. - Tests use
require('../common')(test/common/index.js) for helpers:mustCall,mustNotCall,expectsError,hasCrypto,isWindows, etc. - C++ unit tests live in
test/cctest/. Add new ones vianode.gyp'test_cctest_sources'. - Add new test files; do not extend large existing ones unless the topic genuinely matches.
See How to contribute › Testing for fixtures, runner flags, and CI tiers.
Commit message and PR conventions
The commit message format is enforced by tools/lint-pr-url.mjs and a CI lint:
<subsystem>: short imperative summary, lowercase, ≤ 50 chars
Optional longer explanation, wrapped at 72 chars. Reference fixed
issues with "Fixes: <url>" or "Refs: <url>".
PR-URL: https://github.com/nodejs/node/pull/<n>
Reviewed-By: ...Subsystems are listed in doc/contributing/pull-requests.md § "Appendix: subsystems" and are also enforced by .github/workflows/commit-lint.yml.
For Collaborator-merged PRs the merger usually rewrites commits via node-core-utils (git node land). For external contributors, get the subject right; the rest is added on land.
Documentation expectations
- New JS API surface ⇒ update
doc/api/<module>.md. - New error code ⇒ update
doc/api/errors.md. - New CLI flag ⇒ update
doc/api/cli.mdand thenode.1man page (the doctool generates it from CLI markdown). - The doctool lives in
tools/doc/.make docregenerates HTML+JSON. - Run
make lint-mdandmake lint-md-buildbefore pushing doc changes.
Performance review culture
For changes in hot paths:
- Add or update a benchmark in
benchmark/<area>/. - Run the benchmark before and after; paste numbers into the PR.
- Consider whether you crossed the JS/C++ boundary more than necessary; that is the most common regression cause.
The performance working group lives at @nodejs/performance (per CODEOWNERS).
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.