December Code

JavaScript promises and async/await interview questions, with answers

Asynchronous code is where JavaScript interviews test whether you can predict order, not just recall syntax. The language runs one thing at a time, so every question comes down to when a piece of code gets its turn: before or after a timer, before or after a promise callback, before or after the line that follows an await. The rules are the event loop's, and every answer below states them and then shows them in code. Every output shown was produced by running the code in Node.js. The ordering rules shown are the same in browsers.

The questions start with the event loop and promises, then async and await, then combining, failing and parallelising asynchronous work. Then take the free JavaScript diagnostic — ten questions across every JavaScript topic in the bank — to see which of these you can explain but not yet predict.

The questions, with answers

  1. 1.How does the JavaScript event loop work?

    In short: JavaScript runs one task at a time on a single call stack; when the stack empties, it runs every queued microtask, such as promise callbacks, and only then takes the next task, such as a timer callback.

    Synchronous code runs to completion first. Asynchronous work waits in two kinds of queue. Tasks, often called macrotasks, include timer callbacks, I/O events and user events; microtasks include promise reactions — then, catch, finally and the rest of an async function after an await — and queueMicrotask callbacks. After each task the engine runs microtasks until that queue is empty, including any that microtasks add, and a browser may then render. That is why the microtask below prints before a timer set for zero milliseconds, as in this page's sample, and why an endless chain of microtasks can freeze a page as surely as an infinite loop. Node also has process.nextTick, which runs before other microtasks.

    setTimeout(() =>
      console.log("timer"), 0);
    queueMicrotask(() =>
      console.log("micro"));
    console.log("sync");
    // sync
    // micro
    // timer
  2. 2.What are the states of a promise in JavaScript?

    In short: A promise is pending until it settles, either fulfilled with a value or rejected with a reason, and once settled it never changes state again.

    new Promise(executor) runs the executor immediately and synchronously, passing it resolve and reject; the first of them to be called wins and later calls are ignored, which is why the promise below fulfils with 1, not 2. Resolving with another promise makes this one follow it, which is why resolved and fulfilled are not quite the same word. then(onFulfilled, onRejected) registers reactions and returns a new promise, resolved with whatever the reaction returns, which is what makes chaining work. Reactions always run asynchronously, as microtasks, even when the promise has already settled, so after prints before 1. A rejection that nothing handles triggers an unhandledrejection event in browsers and, in current Node versions, ends the process.

    const p = new Promise((r) => {
      console.log("executor");
      r(1);
      r(2);
    });
    p.then((v) => console.log(v));
    console.log("after");
    // executor
    // after
    // 1
  3. 3.How does async/await relate to promises?

    In short: An async function always returns a promise, and await pauses only that function until a promise settles, handing control back to the caller meanwhile — promise chaining written as sequential code.

    Whatever an async function returns becomes the fulfilment value of its promise, and whatever it throws becomes the rejection. await x converts x to a promise if it is not one, suspends the function and lets the caller continue; when x settles, the rest of the function is queued as a microtask. So code before the first await runs synchronously when the function is called, and code after it runs later, which is why caller prints between start and data below. Errors from awaited promises surface as exceptions, so try/catch works around await. A common bug is a missing await: the function carries on with a pending promise where it expected a value, and a rejection goes unhandled.

    async function load() {
      console.log("start");
      const v = await Promise
        .resolve("data");
      console.log(v);
      return v.length;
    }
    load().then((n) =>
      console.log(n));
    console.log("caller");
    // start
    // caller
    // data
    // 4
  4. 4.What is the difference between Promise.all, allSettled, race and any?

    In short: all fulfils with every value or rejects on the first rejection; allSettled waits for all and reports each outcome; race settles like whichever settles first; any fulfils with the first success.

    Promise.all([a, b, c]) is for work that must all succeed: it keeps results in input order, not completion order, and rejects as soon as any input rejects, while the other operations keep running unobserved. Promise.allSettled never rejects: it resolves to one object per input, with a status and either a value or a reason, as below, which suits batches where partial failure is acceptable. Promise.race settles with the first input to settle either way, the basis of a timeout wrapper. Promise.any ignores rejections unless every input fails, and then rejects with an AggregateError, which suits trying several mirrors and taking the fastest success.

    const ok = Promise.resolve(1);
    const bad = Promise.reject(
      new Error("down"));
    Promise.allSettled([ok, bad])
      .then((rs) => console.log(
        rs.map((x) => x.status)
          .join()));
    // fulfilled,rejected
  5. 5.How do you handle errors in promises and async functions?

    In short: End every promise chain with a catch, or wrap awaits in try/catch; an error thrown anywhere in a chain skips to the next rejection handler, and every chain needs one.

    A rejection travels down a chain, skipping then callbacks, until it reaches a catch or a then with a second argument; a catch that returns normally turns the chain back into a fulfilled one, so recovery is possible. Throwing inside a then callback is the same as returning a rejected promise. With async and await, the same errors appear as exceptions at the await, so try, catch and finally read like synchronous code, as below. Two pitfalls: an error thrown inside a setTimeout callback within a promise executor is not a rejection and escapes the promise entirely, and a promise that nobody awaits or catches becomes an unhandled rejection. finally runs either way and passes the original outcome through.

    async function getUser() {
      throw new Error("404");
    }
    async function main() {
      try {
        await getUser();
      } catch (e) {
        console.log(e.message);
      } finally {
        console.log("done");
      }
    }
    main();
    // 404
    // done
  6. 6.How do you run async operations in parallel instead of one after another?

    In short: Start every operation first and then await them together with Promise.all; awaiting each call in turn inside a loop makes independent requests wait for one another.

    await inside a for loop is sequential by design: each iteration starts only after the previous promise settles, so ten independent 100 ms requests take a second. Creating the promises first, with map, starts all of them at once, and await Promise.all then waits roughly as long as the slowest, which is why the three 50 ms waits below finish in well under 100 ms. Sequential is right when each step needs the previous result or when a server limits concurrency, and in between, a small pool or batches bound how many run at once. forEach with an async callback is a trap: forEach ignores the returned promises, so the code after it runs before any of them finishes.

    const wait = (ms) =>
      new Promise((r) =>
        setTimeout(r, ms));
    async function main() {
      const t = Date.now();
      await Promise.all(
        [50, 50, 50].map(wait));
      const s = Date.now() - t;
      console.log(s < 100);
    }
    main();
    // true
  7. 7.What is callback hell, and how do promises fix it?

    In short: Callback hell is the pyramid of nested callbacks that dependent asynchronous steps produce; promises return an object you chain with then, so the steps line up instead of nesting.

    Callback APIs such as Node's older fs.readFile(path, (err, data) => ...) put the next step inside the call, so each dependent step nests one level deeper and every level has to handle errors itself. Promises invert that: the function returns an object, and you attach what happens next with then, which returns another promise, so the steps form a flat chain and one catch at the end handles an error from any of them. async and await flatten it further into sequential-looking code. Wrapping a callback API in a promise, as below, is a common interview exercise; util.promisify and the fs/promises module do it for Node's APIs. A promise also settles only once, where a buggy callback API might call you twice.

    function later(v, cb) {
      setTimeout(() =>
        cb(null, v * 2), 5);
    }
    const laterP = (v) =>
      new Promise((ok, no) =>
        later(v, (e, r) =>
          e ? no(e) : ok(r)));
    laterP(2)
      .then(laterP)
      .then((r) => console.log(r));
    // 8

How the diagnostic asks it

One question from the JavaScript bank, exactly as a sitting would show it. The bank has 4 on asynchronous javascript and 30 across JavaScript.

Asynchronous JavaScript · mediumJS-023

What does this code print? (The options list the printed lines in order.)

console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
  1. 1A B C D
  2. 2A D B C
  3. 3A D C Bcorrect
  4. 4A C D B

The synchronous code runs first, printing A and D. When the call stack is empty, the event loop runs all queued microtasks, including promise reactions, before the next macrotask such as a timer callback, so C prints before B even though the timeout is 0. A B C D assumes callbacks run where they are written. A D B C puts the timer first. A C D B assumes a resolved promise's then() runs synchronously, but then() callbacks always run asynchronously.

Measure it

Reading answers tells you what’s true. A diagnostic tells you what you get wrong.

10 JavaScript questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.

What the readiness test measures · how the score is computed

By Harshit · updated