Lesson overview · Free interview practice

Promise Patterns

Master advanced Promise patterns and asynchronous flow control.

Topics in the full lesson
  • Introduction to Promises
  • Creating Promises
  • Handling Promises
  • Promise.all and Promise.race
  • Asynchronous Patterns
  • Best Practices
  • Exercises

Practice Problems

Work through a question before revealing its explanation. These questions and answers are free; Premium adds the full lesson walkthrough, examples and implementation detail.

Completion marks record your own progress, not an automatically checked result. The task type does not determine whether it is optional.

TheoryMedium

`Promise.withResolvers` and the Deferred Pattern

Question

What does Promise.withResolvers() return, and what problem does it solve? Show the deferred pattern with and without it.

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

TheoryMedium

Promise Anti-Patterns Catalog

Question

What are the most common Promise anti-patterns, and what does the correct code look like in each case?

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

TheoryHard

Cancellation with `AbortController` and `AbortSignal`

Question

How do you cancel an in-flight async operation with AbortController and AbortSignal? Show integration with fetch, signal composition, and how to adapt non-aware APIs.

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

TheoryHard

Promise Execution Semantics + the Microtask Queue

Question

When does a .then callback actually run relative to surrounding code? Walk through the microtask queue and how await desugars to a microtask checkpoint.

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

CodingHard

Timeout and Retry-with-Backoff Patterns

Implement and check your solution in the stated environment before revealing the answer.

Question

Implement withTimeout(promise, ms) and retry(fn, options) using native Promises. Then explain which production concerns these helpers do not solve.

Before you start

This is core practice for advanced Promise patterns. You need Promise.race, try/finally, timer cleanup, async functions and rejection propagation. Review Asynchronous JavaScript if the combinators are unfamiliar. Use Node 24.21.0 and the companion exercise files. No API account or network request is needed.

Timeout contract

  • Accept a value, Promise or thenable. Return a native Promise that adopts it unless the timer wins.
  • ms must be a finite number from 0 through 2_147_483_647. Invalid delays reject with RangeError. Callers must still handle any independently created operation if they supply invalid options.
  • If the timer wins, reject with Error("Timeout after <ms>ms"), replacing <ms> with the delay. Clear the timer whichever side settles first. A fulfilled input can win against a zero-delay timer.
  • Timers do not guarantee exact wall-clock deadlines. Timing out stops waiting. It does not cancel the underlying operation. Do not add an implicit AbortController.

Retry contract

Call fn(attempt) sequentially with zero-based attempt numbers. It may return a value or thenable, reject, or throw synchronously. Return its first successful value. With retries: 3, allow at most four calls, including attempt 0.

OptionDefaultRequired behavior
retries3Nonnegative safe integer. Counts additional calls.
baseMs100Finite delay in the timeout range.
factor2Finite number at least 1.
jittertrueBoolean. Multiply each capped delay by 0.5 + Math.random() * 0.5 when enabled.
maxMs30_000Finite delay in the timeout range. Caps the delay before jitter.
shouldRetry() => trueSynchronous predicate receiving (error, attempt). A falsy result stops retries.

Validate options before invoking fn. Invalid numeric options reject with RangeError. Invalid function options or jitter reject with TypeError. The first delay is min(baseMs, maxMs). After each wait, multiply that capped delay by factor and cap again. Do not wait after success or final failure. Rethrow the last operation error unchanged when the retry count is exhausted or the predicate refuses. A thrown predicate error becomes the rejection.

The default retries every failure. Choosing transient failures is the caller's job. These helpers provide neither cancellation nor a total elapsed-time budget. fn may remain pending forever. Do not use retries for non-idempotent writes without a separate safety policy.

Try it before revealing the answer

Edit tasks/timeout/starter.mjs and run:

CANDIDATE=starter node --test tasks/timeout/check.mjs

The checks use a virtual timer harness, not wall-clock sleeps. For a function that fails on attempts 0, 1 and 2, then returns "ok" on attempt 3, { baseMs: 10, factor: 2, maxMs: 15, jitter: false } must schedule [10, 15, 15] and return "ok". Also check invalid options, error identity, jitter bounds and an operation that finishes after its timeout.

Starter: tasks/timeout/starter.mjs

export async function withTimeout(promise, ms) {
  throw new Error('Implement withTimeout')
}

export async function retry(fn, {
  retries = 3,
  baseMs = 100,
  factor = 2,
  jitter = true,
  maxMs = 30_000,
  shouldRetry = () => true,
} = {}) {
  throw new Error('Implement retry')
}

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

CodingHard

Implement a Promise from Scratch (A+ State Machine)

Implement and check your solution in the stated environment before revealing the answer.

Question

Implement MyPromise with a then method that follows the Promises/A+ state and resolution requirements. Keep the self-resolution and thenable safeguards. Do not replace them with a happy-path subset or delegate the state machine to native Promise.

Preparation

This is an advanced extension, not a prerequisite for using native Promises. You need closures, private class fields, state machines, exception propagation, thenables and microtask scheduling. Review Event Loop and the Promise examples in this lesson first.

Use Node 24.21.0. The companion packet contains tasks/promise/starter.mjs, focused checks and an adapter for the official promises-aplus-tests package, pinned to 2.1.2 in the isolated fixture manifest. Run npm ci --ignore-scripts --no-audit --no-fund in that fixture directory if its dependencies are absent. Do not install into an application manifest.

Required protocol

  • new MyPromise(executor) runs the executor synchronously with resolve and reject. A nonfunction executor throws TypeError. An executor throw rejects unless a prior call already resolved the promise.
  • Start pending. Settle at most once. The first constructor resolver call locks in its outcome, even while an adopted thenable is still pending. A later reject must not replace it.
  • then(onFulfilled, onRejected) returns a new MyPromise. Call registered handlers in registration order, as functions without a receiver. Schedule handlers with queueMicrotask, including handlers added after settlement.
  • Ignore nonfunction handlers. Propagate the original fulfilled value or rejection reason when its handler is absent. A handler return value resolves the child. A handler throw rejects the child with the same reason.
  • Reject direct or chained self-resolution with TypeError.
  • When adopting another MyPromise, follow its eventual state and preserve its fulfilled value or rejection reason by identity. Do not resolve an already-settled value again. The value's properties can change without changing which object the source promise holds.
  • For other object or function thenables, read then once, catch a throwing getter, call a callable then with the thenable as receiver and recursively adopt its result.
  • At each adoption step, only the first callback counts. Ignore later callbacks and a throw after the winning callback, including when that callback supplied another pending thenable. A throw before either callback rejects.
  • Include catch(onRejected) as then(undefined, onRejected). Native static combinators, finally, subclass species and host unhandled-rejection tracking are not part of this exercise.

Scheduling distinction

Promises/A+ requires asynchronous handlers. It does not require a microtask implementation specifically. This exercise chooses queueMicrotask. Native ECMAScript Promises use host-enqueued jobs, including a job to call an adopted thenable. Do not infer identical native scheduling in every case just because your handlers also use microtasks. A+ specifies then interoperability, not the complete native Promise API.

Read the resolution procedure in the official Promises/A+ specification before implementing. General cycles among foreign thenables need not be detected by A+. Direct self-resolution is mandatory. Do not add an arbitrary maximum adoption depth.

Verification before the reveal

This case must log true, even though the fulfilled object's then property changes:

const value = { then: null }
const source = new MyPromise((resolve) => resolve(value))
value.then = (resolve) => resolve('changed')
source.then(() => source).then((actual) => {
  console.log(actual === value)
})

Observe the value inside a MyPromise handler, as above, or put it inside a plain result object before passing it to native await. Awaiting the changed object itself would invoke native thenable assimilation and test a different behavior.

Edit the starter, then run both:

CANDIDATE=starter node --test tasks/promise/check.mjs
CANDIDATE=starter node tasks/promise/aplus.mjs

The first command checks constructor adoption, settled-value identity, malicious thenables, self-resolution, ordering and deep chains. It covers both already-settled and pending MyPromise adoption. The second runs the full official then conformance suite. The unchanged starter is expected to fail. The official suite alone does not cover every A+ edge case.

Starter: tasks/promise/starter.mjs

export class MyPromise {
  constructor(executor) {
    throw new Error('Implement MyPromise construction and resolution')
  }

  then(onFulfilled, onRejected) {
    throw new Error('Implement asynchronous chaining')
  }

  catch(onRejected) {
    return this.then(undefined, onRejected)
  }
}

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

Explore the full Promise Patterns material

Premium includes the complete lessons and implementation references. Free practice questions remain available without a subscription.

All course tracks & premium content
From basics to advanced masterclasses
Built for JS/TS developers like you
Real-world tips & common pitfalls
Upgrade to Premium