HuntExams Academy logo
JavaScriptIntermediate#async#promises

What is a Promise and what are its states?

Answer

A Promise in JavaScript is an object representing the eventual result of an asynchronous operation. Instead of passing a callback into a function, the function hands you a Promise and you attach handlers with .then(), .catch() and .finally(), or you await it. The three states: 1. pending \u2014 the initial state; the operation has not finished. 2. fulfilled (resolved) \u2014 completed successfully, carrying a value. 3. rejected \u2014 failed, carrying a reason, usually an Error. A promise is settled once it is fulfilled or rejected. Two rules matter in interviews: the transition happens exactly once and is immutable afterwards, so later resolve or reject calls are ignored; and callbacks are queued as microtasks, so they always run after the current synchronous code but before setTimeout macrotasks. Key behaviours to know: - .then() returns a NEW promise, which is why chaining works. Returning a value passes it along, returning a promise waits for it, and throwing rejects the rest of the chain. - .catch(fn) is shorthand for .then(null, fn) and catches rejections from anywhere earlier in the chain. - .finally(fn) runs on both paths and passes the outcome through, which makes it ideal for hiding loaders. - An unhandled rejection fires the unhandledrejection event, so always end a chain with catch or wrap await in try/catch. Combinators: Promise.all (all succeed, or the first rejection wins), Promise.allSettled (never rejects, gives an array of {status, value|reason}), Promise.race (first to settle either way), Promise.any (first fulfilment; rejects with AggregateError only if every promise fails). async/await is syntactic sugar over exactly this machinery \u2014 an async function always returns a promise.

Example

const wait = (ms, value) =>
  new Promise((resolve, reject) =>
    setTimeout(() => (ms < 0 ? reject(new Error('bad ms')) : resolve(value)), ms));

// pending -> fulfilled
wait(100, 'done')
  .then(v => v.toUpperCase())    // returns a NEW promise
  .then(v => console.log(v))     // "DONE"
  .catch(err => console.error(err.message))
  .finally(() => console.log('cleanup runs either way'));

// async/await is the same machinery
async function load() {
  try {
    const res = await fetch('/api/users');
    if (!res.ok) throw new Error(res.status);
    return await res.json();
  } catch (e) {
    console.error('failed', e);
    return [];
  }
}

// Microtask ordering: promise callbacks beat setTimeout
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('microtask'));
console.log('sync');
// sync -> microtask -> timeout

// Combinators
await Promise.all([a, b]);          // rejects fast
await Promise.allSettled([a, b]);   // never rejects
await Promise.race([a, timeout]);   // first to settle
await Promise.any([a, b]);          // first success

Detailed explanation

A Promise in JavaScript is an object representing an eventual outcome, commonly from an asynchronous operation such as a network request or timer. Rather than receiving the result immediately, you attach handlers using .then(), .catch(), and .finally(), or consume the promise using await. Promises make dependent operations easier to compose and give failures a consistent propagation mechanism. They do not themselves create threads or make synchronous work non-blocking.

A promise has three states: pending, fulfilled, and rejected. Pending means no final outcome is available yet; fulfilled means it has a success value; rejected means it has a failure reason, preferably an Error object. Fulfilled and rejected promises are collectively called settled, and their state cannot change again. Importantly, resolved is not an exact synonym for fulfilled: resolving with another pending promise locks in that promise’s eventual outcome while the outer promise remains pending. Once resolution is committed, subsequent calls to resolve or reject cannot override it.

The executor passed to new Promise runs synchronously, but handlers registered through .then(), .catch(), and .finally() run asynchronously through the microtask queue. Even handlers attached to an already-settled promise wait until the current synchronous execution finishes. For example, console.log('sync') runs before a handler registered with Promise.resolve().then(...). In the usual same-turn example, that handler also runs before a zero-delay timer callback. Interviewers often use this ordering to check whether you distinguish promise creation from reaction execution.

Each .then() returns a new promise: returning a plain value fulfills it, returning a promise makes it adopt that outcome, and throwing rejects it. .catch(fn) is equivalent to .then(undefined, fn); returning normally from it recovers the chain. .finally() supports cleanup and normally preserves the original outcome, unless it throws or returns a rejecting promise. An async function always returns a promise, and await turns a rejection into a thrown exception at the suspension point. Handle failures with an appropriate rejection handler or try/catch around await, rather than assuming every chain must end with .catch().

Key points to remember

  • The three states are pending, fulfilled, and rejected; fulfilled and rejected mean settled.
  • Settlement is irreversible, but resolving with a pending promise does not immediately fulfill it.
  • A Promise executor runs synchronously; registered reaction handlers run as microtasks.
  • .then() creates a new promise whose outcome depends on the handler’s return value or thrown exception.
  • .catch() handles rejection, .finally() performs cleanup, and async/await consumes the same promise mechanism.

Common mistakes candidates make

  • Treating resolved and fulfilled as interchangeable, ignoring adoption of another pending promise.
  • Assuming the Promise constructor defers its executor or moves expensive computation to another thread.
  • Forgetting to return a nested promise from a handler, breaking sequencing and rejection propagation.

Likely follow-up questions

How do Promise.all(), Promise.allSettled(), Promise.race(), and Promise.any() differ?

Promise.all() fulfills with ordered results when all inputs fulfill, or rejects on the first observed rejection; Promise.allSettled() waits for every input outcome and returns status records. Promise.race() adopts the first settlement, whereas Promise.any() takes the first fulfillment and rejects with AggregateError if every input rejects. These describe normal iterable inputs; iteration errors can also reject the returned promises.

Does .finally() change the result of a promise chain?

Normally, it preserves the original value or rejection reason, and its callback receives neither. A thrown exception or returned rejecting promise replaces the original outcome with a rejection; a returned pending promise delays completion.

What happens if a promise rejection is not handled?

Browsers can report it through the unhandledrejection event; Node.js has its own unhandledRejection mechanism and runtime-dependent behaviour. Attach a rejection handler or catch the error around await at the layer responsible for recovery or reporting.

Interview tip: Start with the definition and three states, then demonstrate irreversible settlement and microtask ordering with a small example.

Related Interview Questions

1
JavaScriptAdvanced#async#promises

Difference between Promise.all, allSettled, race and any?

Open
2
JavaScriptIntermediate#async

How does async/await work?

Open
3
JavaScriptIntermediate#async#performance

How do you run async calls in parallel with async/await?

Open