Skip to content
Topic 17 of 21
Theory

Timers and asynchrony

Why it matters

Until now programs ran strictly top to bottom: line after line, with no pauses. But real programs constantly need to wait: show a tip after a second, update a clock every second, wait for the server's response, react to a click. Code that runs not right away but later is called asynchronous.

Timers are the simplest asynchronous code. They clearly show JavaScript's main rule: delayed code never interrupts the current code, it waits for its turn. Without understanding this rule you won't understand promises, async/await, or when React updates the page.

setTimeout — run once, later

setTimeout(() => {
  console.log("A second has passed");
}, 1000);

console.log("Timer set");

Output:

Timer set
A second has passed
  • The first argument is the function (callback) to call.
  • The second is the delay in milliseconds: 1000 ms = 1 second.
  • setTimeout doesn't wait: it notes "call this in a second" and returns immediately. That's why "Timer set" is printed first.

You can pass data to the callback through a closure:

function remind(text, ms) {
  setTimeout(() => console.log("Reminder: " + text), ms);
}

remind("drink water", 200);
remind("stretch", 100);

Output:

Reminder: stretch
Reminder: drink water

They fire in order of time, not in the order of the calls.

Cancel: clearTimeout

setTimeout returns a timer id. You can use it to cancel the timer before it fires:

const id = setTimeout(() => console.log("Boom!"), 500);
clearTimeout(id);
console.log("Timer cancelled");

Output:

Timer cancelled

"Boom!" will never be printed.

setInterval — repeat

setInterval(callback, ms) calls the function again and again with an ms pause until it is stopped with clearInterval:

let count = 0;
const id = setInterval(() => {
  count++;
  console.log("Tick " + count);
  if (count === 3) {
    clearInterval(id);
    console.log("Stop");
  }
}, 100);

Output:

Tick 1
Tick 2
Tick 3
Stop

A forgotten clearInterval is a common mistake: the interval will run forever.

Why delayed code waits: the event loop

JavaScript runs code in one thread: at any moment only one function is running. Until it finishes, nothing else can start. It works like this:

  1. First, all the ordinary (synchronous) code runs, top to bottom.
  2. Callbacks that are ready to run (a timer that fired, a click, a server response) join a queue.
  3. When the current code has finished, the event loop takes the next callback from the queue and runs it completely. Then the next one — and so on for as long as the program runs.

That's why even a timer with a delay of 0 doesn't fire right away:

console.log("1");
setTimeout(() => console.log("2"), 0);
console.log("3");

Output:

1
3
2

setTimeout(…, 0) means "as soon as possible, but after the current code". And the delay is the minimum waiting time: if the program is busy with a long loop, the timer waits until it is free.

const start = Date.now();
setTimeout(() => console.log("Timer was late: " + (Date.now() - start >= 100)), 10);

while (Date.now() - start < 100) {
  // a busy loop for 100 ms — the queue waits
}
console.log("Loop finished");

Output:

Loop finished
Timer was late: true

The timer was ready after 10 ms, but it ran only after the loop freed the thread. That's why in the browser heavy computations "freeze" the page: clicks wait in the queue too.

Callbacks and "callback hell"

An asynchronous function can't simply give back its result with return — the result doesn't exist yet. So you ask it: "when it's ready, call this function":

function loadUser(id, callback) {
  setTimeout(() => callback({ id, name: "Ann" }), 50);   // pretend server response
}

loadUser(1, (user) => console.log("Loaded: " + user.name));
console.log("Loading...");

Output:

Loading...
Loaded: Ann

If there are several steps and each waits for the previous one, the callbacks get nested inside each other:

loadUser(1, (user) => {
  loadOrders(user, (orders) => {
    loadDetails(orders[0], (details) => {
      // ...and even deeper
    });
  });
});

This "staircase" is called callback hell: it is hard to read, and errors have to be handled at every level. Promises solve this problem — the next topic.

Debounce: wait until the user stops

A useful trick built on timers. The user types in a search box, and sending a request to the server for every letter is wasteful. Better to wait until they stop typing:

function debounce(fn, ms) {
  let timer;                            // lives in the closure between calls
  return (...args) => {
    clearTimeout(timer);                // a new call cancels the old timer
    timer = setTimeout(() => fn(...args), ms);
  };
}

const search = debounce((q) => console.log("Searching: " + q), 100);
search("j");
search("ja");
search("jav");

Output:

Searching: jav

Three quick calls — one request, with the last value.

How it works inside

Timers are counted not by JavaScript itself but by the environment: the browser or Node.js. When setTimeout is called, the environment remembers the function and the time. When the time comes, it puts the callback into the task queue. The JavaScript engine runs the synchronous code to the end, and then the event loop takes tasks from the queue one at a time.

Three consequences follow:

  • a timer callback never runs in the middle of another function;
  • the delay means "not earlier than", not "exactly after";
  • while there are no callbacks, the program simply waits. In Node.js the program exits when the queue is empty and no timers are left.

Common mistakes

  1. Calling instead of passing. setTimeout(sayHi(), 1000) calls sayHi right away and gives the timer its result. You need setTimeout(sayHi, 1000) or setTimeout(() => sayHi(), 1000).
  2. Waiting for a result with return. A value you get inside a timer callback can't be returned from the outer function: it has already finished. You need a callback (or a promise).
  3. Forgot clearInterval. An interval keeps running until it is stopped.
  4. Seconds instead of milliseconds. setTimeout(fn, 5) is 5 ms, not 5 seconds.
  5. Thinking 0 means "immediately". The callback runs only after all the current code.

Cheat sheet

What How
Run once later const id = setTimeout(fn, ms)
Cancel clearTimeout(id)
Repeat const id = setInterval(fn, ms)
Stop repeating clearInterval(id)
After the current code setTimeout(fn, 0)
Order first all synchronous code, then the queue
Debounce clearTimeout(timer); timer = setTimeout(...)

Delayed greeting

Easy+10 XP

Write a function greetLater(name, ms, callback). It does not return a result right away; instead, after ms milliseconds it calls callback with the string Hello, <name>!.

greetLater("Ann", 100, (text) => console.log(text));
console.log("waiting...");

Output:
waiting...
Hello, Ann!        ← after 100 ms

Connect the connector to solve tasks

Task code runs on your own computer — faster and more reliable than in the browser. This needs the free CodeWithMe connector app. You only connect it once.

Looking for the connector on this computer…

  1. 1
    Download the connector for your system (Windows, macOS or Linux).Download the connectorOn the download page pick the file with your system in its name, e.g. windows-amd64.
  2. 2Run it and choose a folder for your solutions. It will install Python and Node.js there by itself when needed.
  3. 3
    Enter below the 6-digit code shown in the connector window (“Code for CodeWithMe”). If the browser asks about local network access — allow it.

The connector is open source (on GitHub), works only on your computer and runs code only after you enter the pairing code.