Open-Source Wikis

/

Node.js

/

How to contribute

/

Patterns and conventions

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 call arr.map, obj.hasOwnProperty, str.startsWith, etc. directly. Use ArrayPrototypeMap(arr, fn), ObjectPrototypeHasOwnProperty(obj, k), StringPrototypeStartsWith(str, prefix).
  • Use SafeMap, SafeSet, SafeWeakMap, SafeFinalizationRegistry, SafeArrayIterator instead of new Map(), etc., when you need iteration that does not depend on a possibly-replaced Symbol.iterator.
  • The exception is lib/internal/per_context/primordials.js itself, which uses /* eslint-disable node-core/prefer-primordials */ because it is doing the capturing.
  • Tests in test/ and benchmarks in benchmark/ 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/*.js are thin re-export shells that pull from lib/internal/<name>.js (or from a directory like lib/internal/<name>/).
  • New built-in JS code lives under lib/internal/, never the top level.
  • All built-in .js files start with 'use strict'; and use single-quoted strings.
  • The __proto__: null idiom 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 (BuiltinModule in lib/internal/bootstrap/realm.js) requires that require('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 in lib/internal/bootstrap/**. Read those from lib/internal/process/pre_execution.js instead, 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#deferredAccessors is 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.h headers hold inline implementations. foo.h declares; foo-inl.h defines inline; foo.cc defines non-inline. Including patterns matter: foo.cc includes foo-inl.h; users of foo include only foo.h.
  • Use node::Environment*/node::Realm* rather than capturing v8::Isolate* directly. The right Environment for an isolate is Environment::GetCurrent(args).
  • Wrap libuv handles in HandleWrap subclasses; one-shot requests in ReqWrap. Anything that needs a JS callback inherits from AsyncWrap. See primitives › async-wrap-and-handle-wrap.
  • Use BaseObjectPtr<T> (a strong reference) and BaseObjectWeakPtr<T> (a weak reference) instead of raw pointers across async boundaries.
  • Bindings are registered with NODE_BINDING_CONTEXT_AWARE_INTERNAL(<name>, Initialize) (see src/node_binding.cc). Initialization functions install JS-visible function templates; per-realm storage lives in a BindingData struct.
  • Externally-visible C APIs go through src/node.h, src/js_native_api.h, or src/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 forked cpplint (tools/cpplint.py, configured in .cpplint). make format-cpp and make lint-cpp are the canonical commands.

Testing conventions

  • Most tests live in test/parallel/test-<topic>-<scenario>.js and 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 in test/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 via node.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.md and the node.1 man page (the doctool generates it from CLI markdown).
  • The doctool lives in tools/doc/. make doc regenerates HTML+JSON.
  • Run make lint-md and make lint-md-build before 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.

Patterns and conventions – Node.js wiki | Factory