Synchronous, Asynchronous and the Event Loop

First lesson of the Asynchronous JavaScript (Promises, fetch, APIs) course, free to read: about 15 minutes, 3 exercises and 3 quiz questions. This is the catalogue's base lesson; in the student space, it is rewritten for your profile.

Objectives

  • Tell synchronous code from asynchronous code
  • Explain the role of the call stack, the task queue and microtasks
  • Predict the execution order of a program that uses setTimeout and callbacks

Event loopCallback functionssetTimeout

Synchronous, Asynchronous and the Event Loop

JavaScript runs your code on a single thread of execution. It does only one thing at a time: the function currently running sits at the top of the call stack, and the next one waits for it to finish.

function saluer(nom) {
  return `Hello ${nom}`;
}
console.log("start");
console.log(saluer("Ada")); // saluer is pushed, executed, then popped
console.log("end");
// Prints: start, Hello Ada, end (in that order)

Why asynchronous?

Waiting for a network response can take a second. If JavaScript blocked the thread during that time, the page would freeze: no more clicks, no more animation. The solution: delegate the waiting to the environment (browser or Node.js) and hand it a function to call back later, a callback.

console.log("A");
setTimeout(() => {
  console.log("B"); // runs later, by the event loop
}, 0);
console.log("C");
// Prints: A, C, B

Even with a delay of 0, B is printed last. setTimeout does not run the function: it asks the environment to put it in the task queue once the delay has elapsed.

The event loop

The event loop continuously repeats this cycle:

  1. run the code in the call stack until it is empty;
  2. run all pending microtasks (promise continuations, queueMicrotask);
  3. take one task from the queue (a setTimeout callback, a click event…) and start over.
setTimeout(() => console.log("task"), 0);
Promise.resolve().then(() => console.log("microtask"));
console.log("synchronous");
// synchronous, microtask, task

The setTimeout delay is therefore a minimum, not a guarantee: if the thread is busy with a long computation, the callback will wait.

The callback style

Before promises, asynchronous APIs took a callback, often following the Node.js “error-first” convention:

function lireConfig(callback) {
  setTimeout(() => {
    const ok = true;
    if (!ok) return callback(new Error("File not found"));
    callback(null, { theme: "dark" }); // null = no error
  }, 10);
}

lireConfig((err, config) => {
  if (err) return console.error(err.message);
  console.log(config.theme);
});

This style works, but as soon as several operations depend on each other, callbacks nest: this is callback hell. Each level has to handle its own errors, and the code drifts to the right. Promises, covered in the next lesson, solve this problem by representing a future result as an object you can chain.

Key takeaways

  • Only one thing at a time on the call stack.
  • Waiting is delegated; the continuation comes back through the task queue or the microtask queue.
  • Microtasks run before tasks.

Exercises

Solutions and tests stay on the server. In the student space, your code is run and checked, and the tutor gives hints before the solution.

  1. CodeDifficulty 1JavaScript in the browser1 hint

    Predict the output order

    The ordre() function must return a promise that resolves with the array of labels in the actual order of execution. Complete the code to record each label in journal and then resolve the promise after the timer.

    Observe the resulting order: synchronous, microtask, task.

    function ordre() {
      const journal = [];
      return new Promise((resolve) => {
        setTimeout(() => {
          journal.push("timeout");
          // TODO : résoudre la promesse avec journal
        }, 0);
        Promise.resolve().then(() => journal.push("promesse"));
        journal.push("synchrone");
      });
    }
  2. CodeDifficulty 2JavaScript in the browser2 hints

    The "error-first" convention

    Write diviserPlusTard(a, b, callback) which, after a setTimeout of 0 ms, calls:

    • callback(new Error("Division par zéro")) if b === 0;
    • otherwise callback(null, a / b).
    function diviserPlusTard(a, b, callback) {
      // TODO
    }
  3. Multiple choiceDifficulty 11 hint

    Microtask or task?

    What does this code print?

    setTimeout(() => console.log('X'), 0);
    Promise.resolve().then(() => console.log('Y'));
    console.log('Z');
    • X, Y, Z
    • Z, Y, X
    • Z, X, Y
    • Y, Z, X

End-of-lesson quiz

  1. How many JavaScript instructions run at the same time on the call stack of a single thread?

    • Just one
    • As many as there are processor cores
    • Two: one synchronous and one asynchronous
  2. What does the delay passed to setTimeout guarantee?

    • Execution exactly after that delay
    • A minimum delay before the callback can run
    • Execution before all promises
  3. What problem does “callback hell” refer to?

    • A callback that is never called
    • Deeply nested callbacks that make the code unreadable
    • A syntax error in setTimeout

The rest of the course

  1. Promises: Creating, Chaining and Handling Errors18 min
  2. async/await and try/catch16 min
  3. Combining Promises: all, allSettled, race, any15 min
  4. fetch: Consuming a JSON API20 min

Asynchronous JavaScript (Promises, fetch, APIs) · Intermediate · 2 h 24 min