Skip to main content
CodeOath
← All posts

JavaScript140 min total · 15 parts

JavaScript Core Concepts: Scope, Closures, `this`, and the Event Loop

Part 9 of 15 · ~3 min

Synchronous vs. Asynchronous Execution

We left the search box frozen. Let us unfreeze it, and then look very carefully at what the fix actually did — because almost everyone's first mental model of it is wrong, and the wrong model is the source of most async confusion later.

Here is the broken version again:

function handleInput(event) {
  spinnerEl.hidden = false;                                  // "show the spinner"
  const matches = fuzzyScore(ARTICLES, event.target.value);  // 400ms of synchronous work
  renderRows(matches);
  spinnerEl.hidden = true;                                   // "hide the spinner"
}

And here is the fix, which is three lines of rearrangement and nothing else:

function handleInput(event) {
  spinnerEl.hidden = false;      // 1. ask for the spinner
  setTimeout(() => {             // 2. hand the stack back to the browser
    const matches = fuzzyScore(ARTICLES, event.target.value);
    renderRows(matches);
    spinnerEl.hidden = true;     // 3. take the spinner away again
  }, 0);
}

The spinner now appears. Same work, same 400 milliseconds, same one thread. The only thing that changed is that handleInput returns almost immediately instead of sitting on the stack for the whole computation — and an empty stack is the browser's cue that it may go and paint.

This is what "asynchronous" means in JavaScript: not "on another thread", but "later" — specifically, after the current call stack has emptied. There is still exactly one stack, exactly one thread, and exactly one thing happening at a time. What asynchrony gives you is a say in when your code takes its turn.

The 0 deserves a hard look, because it is the single most misread argument in the language:

function handleInput(event) {
  console.log("A: handler starts");
  setTimeout(() => console.log("C: the deferred scoring"), 0);
  console.log("B: handler returns");
}
// A, B, C — every time, on every machine, under any load

setTimeout(fn, 0) does not mean "run fn in zero milliseconds." It means "put fn in a queue, and run it once the stack is empty and it reaches the front." Zero is a minimum wait, not an appointment. Synchronous code always finishes first, no matter how small the number you wrote.

Now the part that the fix does not do, which matters more than the part it does.

Deferring work is not the same as parallelising it. Once that setTimeout callback starts running, fuzzyScore is back on the stack and the page is blocked again for the full 400 milliseconds. Clicks during that window queue up and fire late. The spinner is visible, but if it is a CSS animation it will sit perfectly still, because animating requires frames and frames require an empty stack. We did not make the page responsive; we bought exactly one paint, at the start.

Genuinely removing a long computation from the main thread means either cutting it into chunks that each hand the stack back between them, or moving it off-thread entirely with a Web Worker. Neither is what setTimeout does.

Which points at the real fix for our search box, and at the rest of this reference. The scoring should not be on the client at all — the server has the whole index and a better machine to run it on. So we replace fuzzyScore with a network request:

function handleInput(event) {
  spinnerEl.hidden = false;
  const results = fetchResults(event.target.value); // returns instantly — with what?
  renderRows(results);                              // renders nothing useful
}

A network round trip takes somewhere between 40 milliseconds and forever, and fetchResults has to return something right now, on this line, before any of that has happened. It cannot return the results, because there are no results yet.

What it returns instead is a placeholder for results that do not exist yet. That placeholder is a Promise.