Node.js starts I/O, keeps the JavaScript thread available, and settles a callback or promise when the operation completes. Correct async code controls ordering, concurrency, cancellation, errors, and resource cleanup explicitly.
Network, file, database, DNS, and timer operations can finish later than the line that starts them. Node.js delegates supported work to the operating system or its worker pool and continues processing other ready work. This is concurrency: tasks overlap in time even though ordinary JavaScript callbacks run on one event-loop thread.
An asynchronous API does not make CPU-heavy JavaScript nonblocking. A long loop, large synchronous parse, regular-expression blowup, or synchronous file call still occupies the event-loop thread. Move genuinely CPU-bound work to a worker thread and keep request handlers short.
Classic Node APIs call a final callback as callback(error, value). The error is null on success; on failure, handle it before reading the value. Returning from the callback returns only from that callback, not from the outer function that already completed.
Callbacks remain valid for event-driven APIs and compatibility boundaries, but nested dependent callbacks obscure ownership and error propagation. Prefer promise-native APIs such as node:fs/promises for new sequential workflows. Wrap an old API only when its callback contract is known.
A promise is pending, fulfilled, or rejected. An async function always returns a promise; return becomes fulfillment and throw becomes rejection. await pauses that async function, not the entire process, until the promise settles.
Use try/catch around the operation whose failure you can translate, retry, or compensate. Do not catch an error merely to log and continue with invalid state. Preserve the original error with Error cause when adding domain context.
import { readFile } from 'node:fs/promises';
async function readConfig(path) {
try {
const text = await readFile(path, 'utf8');
return JSON.parse(text);
} catch (error) {
throw new Error(`Cannot load config: ${path}`, { cause: error });
}
}
console.log((await readConfig('./package.json')).name);
The function awaits both file I/O and parsing, then adds safe operation context without discarding the original cause.
Await sequentially when the next operation depends on the previous result or when ordering protects a resource. Start independent promises together and await Promise.all when every result is required. Promise.all rejects on the first rejection but does not cancel operations already started.
Use Promise.allSettled when every outcome must be recorded. Promise.race reports the first settlement, while Promise.any reports the first fulfillment. None of these methods limits concurrency; processing thousands of items at once can exhaust sockets, memory, file descriptors, or a downstream service.
AbortController coordinates cancellation for APIs that accept a signal, including fetch, timers promises, and selected file or stream operations. Pass one signal through the call chain so client disconnect, shutdown, or a deadline can stop cooperative work.
A timeout should abort the underlying operation, not only reject a wrapper while work continues unseen. Always remove custom abort listeners and release handles in finally. Aborting a request cannot roll back an external write that already committed, so mutations still need idempotency or transaction rules.
async function loadUser(id) {
const signal = AbortSignal.timeout(2000);
const response = await fetch(`https://api.example.test/users/${id}`, { signal });
if (!response.ok) throw new Error(`Upstream returned ${response.status}`);
return response.json();
}
The deadline is passed to fetch, so the network operation receives cancellation instead of leaving an orphaned request.
A promise started without await, return, or a deliberate catch can reject outside the visible control flow. Modern Node.js treats an unhandled rejection as an uncaught exception under the default throw mode. Do not rely on a process-level handler to make the application safe to continue.
Use finally for deterministic cleanup such as releasing a database connection, closing a file handle, or clearing a timer. At the process boundary, record fatal context, stop accepting new work, attempt bounded cleanup, and let a supervisor restart the process.
When work appears stuck, inspect whether a promise was never settled, a stream was not consumed, a callback was registered after an event, or a resource remained referenced. Run with source maps, capture the complete cause chain, and attach a request or operation ID across awaits.
Test rejection paths, cancellation before and during work, partial success, cleanup failure, and concurrency pressure. A happy-path await proves syntax; these boundary tests prove ownership.
No. It pauses only the current async function while other ready event-loop work can run. Synchronous JavaScript before or after the await can still block the loop.
Its input promises are usually already started. Promise.all only aggregates their results, so a separate queue or worker limit is needed for large collections.
Explore 500+ free tutorials across 20+ languages and frameworks.