Understanding async/await by Building a Tiny Task Runner

- Estimated reading time: 2 minutes — JavaScript

async and await make asynchronous code read like ordinary code, but it is easy to use them without really knowing what happens underneath. The best way I know to understand them is to build something small.

A task that takes time

First, a helper that waits for a number of milliseconds. It returns a promise: an object that will finish later.

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

A “task” in our runner is just a function that returns a promise:

const task = (name, ms) => async () => {
  await sleep(ms);
  console.log(`${name} finished after ${ms} ms`);
  return name;
};

One after another

await pauses the function until the promise finishes. Awaiting tasks in a loop runs them in order:

async function runInOrder(tasks) {
  const results = [];
  for (const t of tasks) {
    results.push(await t());
  }
  return results;
}

Three tasks of 300, 200 and 100 ms take about 600 ms in total.

All at once

Promise.all starts every task immediately and waits for all of them:

async function runTogether(tasks) {
  return Promise.all(tasks.map((t) => t()));
}

The same three tasks now take about 300 ms, the length of the slowest one.

A few at a time

Starting a thousand network requests at once is rude to the server. A small runner can limit how many tasks run together:

async function runLimited(tasks, limit) {
  const results = [];
  let next = 0;

  async function worker() {
    while (next < tasks.length) {
      const index = next++;
      results[index] = await tasks[index]();
    }
  }

  await Promise.all(Array.from({ length: limit }, worker));
  return results;
}

Each worker picks the next waiting task as soon as it finishes its current one. With limit = 2, never more than two tasks run at the same time.

What to remember

Pattern Time for 300 + 200 + 100 ms Use it when
await in a loop ~600 ms Each step needs the previous result
Promise.all ~300 ms Tasks are independent and few
Limited runner between the two Tasks are independent and many

Errors

Promise.all rejects as soon as one task fails. Use Promise.allSettled if you want every result, including the failures.