Menu
CoddyTech

Async JavaScript Interview Questions

Callbacks, promises, async/await and the event loop, including the ordering questions.

12 questions
  1. What is callback hell and how do you avoid it?

    Callback hell is async steps nested inside each other's callbacks, so the code drifts right and error handling repeats at every level.

    getUser(id, (err, user) => {
      if (err) return handle(err);
      getOrders(user, (err, orders) => {
        if (err) return handle(err);
        getInvoice(orders[0], (err, invoice) => {
          if (err) return handle(err);
          send(invoice);
        });
      });
    });
    

    Rewrite it with async/await and one try/catch, and run independent steps with Promise.all. More in callbacks.

  2. What is a promise in JavaScript?

    A promise stands for a value that will be ready later. It is pending, fulfilled or rejected, and once settled it never changes; read it with .then, .catch or await.

    JavaScript
    const p = new Promise(resolve => setTimeout(() => resolve('done'), 10));
    console.log('pending first');
    p.then(v => console.log('then:', v)).finally(() => console.log('settled'));

    .then returns a new promise, which is what makes chaining work. See promises.

  3. How do async and await work, and how do you handle errors with them?

    An async function always returns a promise. await pauses the function until a promise settles, then gives you the value or throws the rejection, so errors use an ordinary try/catch.

    JavaScript
    function getUser(id) {
      return new Promise((resolve, reject) =>
        setTimeout(() => (id > 0 ? resolve({ id, name: 'Ada' }) : reject(new Error('bad id'))), 10));
    }
    async function show(id) {
      try {
        const user = await getUser(id);
        console.log('got', user.name);
      } catch (err) {
        console.log('failed:', err.message);
      } finally {
        console.log('done', id);
      }
    }
    show(1).then(() => show(-1));

    await pauses only the current function, not the thread. See async and await.

  4. Explain the event loop in JavaScript.

    JavaScript runs your code on one call stack. When the stack empties, the event loop runs every queued microtask (promise callbacks, code after an await), then the next macrotask (a timer, I/O, a UI event), draining microtasks again after each one.

    JavaScript
    console.log('script start');
    setTimeout(() => console.log('timeout'), 0);
    Promise.resolve().then(() => console.log('promise'));
    queueMicrotask(() => console.log('microtask'));
    console.log('script end');

    So timeout prints last, after both microtasks. Diagrams in the event loop.

  5. In what order do setTimeout(fn, 0) and Promise.then callbacks run?

    JavaScript
    const log = [];
    log.push('A');
    setTimeout(() => log.push('B'), 0);
    Promise.resolve().then(() => log.push('C'));
    log.push('D');
    setTimeout(() => console.log(log.join(' ')), 10);

    Synchronous code runs first, pushing A and D. Then the microtask queue drains (C), and only then does the timer run (B). Microtasks always run before the next macrotask, even with a 0 delay.

  6. What is the difference between Promise.all, allSettled, race and any?

    all fulfills when every promise does and rejects on the first rejection; allSettled waits for all and never rejects; race settles like the first one to settle; any fulfills with the first success and rejects with an AggregateError only when all reject.

    JavaScript
    const ok = (v, ms) => new Promise(r => setTimeout(() => r(v), ms));
    const fail = (msg, ms) => new Promise((_, rej) => setTimeout(() => rej(new Error(msg)), ms));
    
    async function main() {
      console.log('all:', (await Promise.all([ok('a', 20), ok('b', 10)])).join(','));
      try {
        await Promise.all([ok('a', 20), fail('boom', 10)]);
      } catch (e) {
        console.log('all rejected:', e.message);
      }
      const settled = await Promise.allSettled([ok('a', 20), fail('boom', 10)]);
      console.log('allSettled:', settled.map(r => r.status).join(','));
      console.log('race:', await Promise.race([ok('slow', 30), ok('fast', 10)]));
      console.log('any:', await Promise.any([fail('x', 5), ok('first ok', 20)]));
    }
    main();

    Typical uses: all when every result is needed, race for timeouts.

  7. What is the difference between awaiting in sequence and in parallel?

    Two awaits in a row run one after the other, so the times add. Starting both and awaiting Promise.all runs them together, so you wait only for the slowest.

    JavaScript
    const wait = ms => new Promise(r => setTimeout(r, ms));
    const elapsed = t => Math.round((Date.now() - t) / 100) * 100;
    
    async function main() {
      let t = Date.now();
      await wait(100);
      await wait(100);
      console.log('sequential ~', elapsed(t), 'ms');
    
      t = Date.now();
      await Promise.all([wait(100), wait(100)]);
      console.log('parallel ~', elapsed(t), 'ms');
    }
    main();

    A common bug is array.forEach(async ...), which waits for nothing; use for...of or Promise.all(array.map(...)).

  8. How do you convert a callback function into one that returns a promise?

    Wrap the call in new Promise and resolve or reject from the callback. A generic promisify does this for any function with a Node-style (err, result) callback.

    JavaScript
    function promisify(fn) {
      return (...args) =>
        new Promise((resolve, reject) =>
          fn(...args, (err, result) => (err ? reject(err) : resolve(result))));
    }
    function divideLater(a, b, cb) {
      setTimeout(() => (b === 0 ? cb(new Error('divide by zero')) : cb(null, a / b)), 10);
    }
    const divide = promisify(divideLater);
    divide(10, 2).then(r => console.log('result', r));
    divide(1, 0).catch(e => console.log('error', e.message));

    Node.js ships the helper as util.promisify.

  9. What happens to a promise rejection that nobody handles?

    If a rejected promise has no handler when the microtask queue drains, the runtime reports an unhandled rejection: browsers log it and fire unhandledrejection, and Node.js 15 and later crash the process by default unless an unhandledRejection handler is set (see --unhandled-rejections).

    Common causes are a promise started with no await or .catch, async callbacks passed to forEach, and return promise inside try without await, which that catch never sees. Fix it where the promise is created.

  10. Can microtasks block the event loop?

    Yes. The loop drains every microtask, including ones queued during the drain, before the next macrotask, so an endless chain starves timers, I/O and rendering.

    JavaScript
    let ticks = 0;
    setTimeout(() => console.log('timer ran after', ticks, 'microtasks'), 0);
    function spin() {
      if (++ticks < 100000) queueMicrotask(spin);
    }
    spin();

    Split long work with setTimeout (or setImmediate in Node.js) between chunks, or move it to a worker.

  11. Does code after await null run before the caller continues?

    JavaScript
    const log = [];
    async function f() {
      log.push('f1');
      await null;
      log.push('f2');
    }
    f();
    log.push('main');
    setTimeout(() => console.log(log.join(' ')), 0);

    An async function runs synchronously until its first await, so f1 goes in first. await null schedules the rest as a microtask and returns to the caller, which pushes main. Then f2 runs, before the timer prints.

  12. How do you cancel a fetch request or other async work?

    Pass an AbortController's signal to fetch and call controller.abort(); the fetch rejects with an AbortError. A promise cannot be cancelled, but the signal stops the work behind it.

    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 5000);
    
    try {
      const res = await fetch('/api/search?q=js', { signal: controller.signal });
      console.log(await res.json());
    } catch (err) {
      if (err.name === 'AbortError') console.log('cancelled');
      else throw err;
    } finally {
      clearTimeout(timer);
    }
    

    AbortSignal.timeout(5000) replaces the manual timer; its error is a TimeoutError, as MDN's AbortSignal.timeout page shows.

Coddy programming languages illustration

Learn to code with Coddy

Get started