Course outline

Node.js / Basic: the Node.js runtime / Lesson 3

Promises and the event loop

Reason about completion, sequencing, and cancellation without assuming that asynchronous code executes in parallel.

About 4 min readContent reviewed
On this page

Lesson goals

  • Explain the concepts and trade-offs behind promises and the event loop.
  • Predict and verify both examples, including their outputs and cleanup behavior.
  • Diagnose the three common failures and apply the worked solutions to related inputs.

One callback at a time

JavaScript in one Node isolate normally executes one callback at a time. Node can wait for many I/O operations while that thread is free, then run completion callbacks when work is ready. Concurrency means overlapping lifetimes; parallelism means simultaneous execution. An async function does not automatically move a computation to another CPU. A long loop before its first await still blocks other JavaScript. This explains why a server can handle many waiting connections efficiently yet become unresponsive when one request performs a large synchronous calculation.

A promise represents an outcome

A promise eventually fulfills with a value or rejects with a reason. await pauses the current async function until that outcome, while other eligible work can continue. Returning a promise from a helper lets its caller own completion and failure. Starting work without returning or awaiting it disconnects that ownership and often creates unhandled rejections. Wrap awaited work in try/catch where the application can make a useful decision. finally is appropriate for cleanup that must happen for both success and failure, but cleanup errors must not casually hide the original problem.

Observe guaranteed ordering

The ordering example records synchronous statements before a promise reaction and a timer. It deliberately avoids comparing setTimeout with setImmediate at program startup, where a universal ordering claim would be misleading. A timer delay is a threshold, not a reservation for an exact instant. Busy JavaScript and other work can postpone it. Microtask queues also matter: repeatedly scheduling more microtasks can starve work that needs the loop to advance. Learn the guarantees your algorithm needs rather than memorizing one diagram as a complete specification of every environment.

javascript Complete example: Observe guaranteed ordering
import { setTimeout as delay } from 'node:timers/promises';
const order = ['sync'];
Promise.resolve().then(() => order.push('promise'));
setTimeout(() => order.push('timer'), 0);
order.push('after scheduling');
await delay(20);
console.log(order.join(' -> '));
Expected output
sync -> after scheduling -> promise -> timer

The current synchronous work completes before the promise reaction, and the timer follows. The 20 ms wait is only a demonstration wait, not a timing benchmark.

Start independent work intentionally

Awaiting one operation and then starting another makes them sequential. Starting both and awaiting Promise.all allows overlap. Promise.all preserves input order in its result, even if operations finish in another order, and rejects when one input rejects. It does not automatically cancel the remaining operations. Use Promise.allSettled when every individual outcome is required. Neither helper limits concurrency, so mapping a million items into promises can exhaust resources. Small independent groups are reasonable; larger workloads need a queue or a bounded worker pattern introduced later in the course.

javascript Complete example: Start independent work intentionally
import { setTimeout as delay } from 'node:timers/promises';
const controller = new AbortController();
const waiting = delay(1000, 'ready', { signal: controller.signal });
controller.abort();
try { await waiting; } catch (error) { console.log(error.name); }
const values = await Promise.all([Promise.resolve(4), Promise.resolve(6)]);
console.log(values.join(','));
Expected output
AbortError
4,6

Aborting rejects the timer and removes its wait. Promise.all returns values in the order provided.

Cancellation is a cooperative contract

AbortController supplies a signal, but only operations that observe that signal can stop. The second example uses the promise-based timer, which supports cancellation, and handles its AbortError explicitly. Cancellation is different from business failure: it may mean a caller no longer needs a result. Pass signals through every supporting layer and release listeners or timers when work completes. Racing a promise against a timeout only stops waiting; the underlying operation can continue unless it receives cancellation. Design a time budget and resource cleanup together so that abandoned work does not accumulate behind apparently fast responses.

Common errors and fixes

Unhandled promise rejection

Why it happens
A launched promise had no awaited or returned owner.
How to fix it
Return or await it and handle failure at a meaningful boundary.
Prevent it next time
Review every asynchronous call for explicit ownership.

The timer fires late

Why it happens
Synchronous CPU work prevented the loop from progressing.
How to fix it
Reduce, partition, or offload the computation.
Prevent it next time
Measure loop delay under realistic load.

Timeout does not stop work

Why it happens
Promise.race only stopped awaiting the original operation.
How to fix it
Pass an AbortSignal to an operation that supports it.
Prevent it next time
Specify cancellation and cleanup in helper contracts.

Check your understanding

1. Two independent reads take about 30 ms each. Explain sequential and concurrent execution without promising exact duration.

Show worked solution

Sequential code starts the second read after the first finishes, so their waiting times add. Start both reads first and await Promise.all to overlap their waits. The concurrent elapsed time can approach the slower read, but scheduling and resource contention can increase it.

2. An API caller disconnects while a supported upstream request is pending. What should happen?

Show worked solution

Abort a controller owned by that request, pass its signal upstream, classify the cancellation separately, and release associated listeners. Do not retry abandoned work automatically. Verify in a test that no response write or background timer remains after cancellation.

Key takeaways

  • JavaScript in one Node isolate normally executes one callback at a time.
  • A promise eventually fulfills with a value or rejects with a reason.
  • AbortController supplies a signal, but only operations that observe that signal can stop.

Official references