Dataslope logoDataslope

Asynchronous Thinking

Why async exists, what "callback" means, and how to think about code that doesn't happen in a straight line.

Most of the code you've written so far executes top-to-bottom, one line after another. Asynchronous code breaks that rule — some work is started now but finishes later. To use it effectively, you need a different mental model.

This chapter builds the intuition first, without the syntax. The next chapter introduces Promises and async/await.

Why async exists at all

Many real things take time:

  • Reading a file from disk.
  • Asking another computer for data (an HTTP request).
  • Waiting for the user to click a button.
  • Waiting for a timer to fire.

A synchronous version of "fetch some data" would mean: stop the entire program until the data arrives. With a single-threaded language like JavaScript, that's catastrophic — the UI freezes, no other code runs, and your one CPU sits idle waiting.

The async philosophy is:

Start the slow work, don't wait, do something else, and arrange to be notified when it's done.

A program that adopts this style stays responsive. It can overlap many slow operations without blocking, even on a single thread.

A non-blocking everyday metaphor

Imagine ordering at a café:

  • Synchronous (bad): you order, then stand at the counter staring, refusing to move until your drink is ready. No one else can be served.
  • Asynchronous (good): you order, get a buzzer, sit down, and do something else. The buzzer fires when your drink is ready.

JavaScript's runtime is the café. Your code is the customer. The buzzer is a callback or a promise.

Callbacks: "tell me when you're done"

A callback is a function you give to another function so it can be called back later. Many older async APIs use this pattern.

Code Block
JavaScript ES2023+

Read the output carefully: 1, 2, 3. The arrow function we gave setTimeout is a callback. We didn't call it ourselves — we gave it to setTimeout to call later.

This is the heart of async thinking:

You're not writing code that "fetches data". You're writing code that says "when the data arrives, do this."

A simulated fetch

Real fetching needs the network. We can simulate the shape with a timer.

Code Block
JavaScript ES2023+

A few important conventions in the old-style callback world:

  • The callback is the last parameter.
  • The first argument is the error (or null if there wasn't one).
  • The remaining arguments are the result.

This is called the Node-style or error-first callback pattern.

The pain: callback hell

Callbacks scale poorly. When one async result feeds the next, you end up with deeply nested code.

Code Block
JavaScript ES2023+

This "rightward drift" is famously called callback hell or the pyramid of doom. Worse, error handling has to be repeated at every level. Promises (next chapter) were invented specifically to flatten this.

Concurrency without parallelism

A subtle but important word distinction:

  • Concurrent: many tasks are in progress during overlapping periods of time. They don't have to run simultaneously.
  • Parallel: many tasks literally run at the same time on separate CPU cores.

JavaScript gives you concurrency on a single thread. While one async task is "in flight" (waiting for the host to do something), your code can start another one. They make progress together, but only one bit of JS is executing at any given moment.

The JS thread is only ever busy in the short bursts; the long waits happen elsewhere.

Order is not what it looks like

This is the biggest mental shift. The order code is written in isn't always the order it runs in.

Code Block
JavaScript ES2023+

Output is A C E B D. The setTimeout callbacks wait until the synchronous code finishes. Reading this requires you to mentally split the code into:

  • the synchronous "now" part: A, C, E
  • the asynchronous "later" parts: B, D

If your prediction was A B C D E, you were reading the code as a story. Async code isn't a story — it's a schedule.

A "do N things and then continue" pattern

A common need: start several async tasks, do something once they're all done. With callbacks alone, this requires manual counting.

Code Block
JavaScript ES2023+

Counting-down patterns like remaining-- are easy to get wrong. That's another problem Promises solve: Promise.all does this counting for you.

Mental model checklist

Before you write any async code, ask:

  1. What slow thing am I waiting for? (timer, network, file, user input, another promise…)
  2. What should run when it finishes successfully?
  3. What should run if it fails?
  4. Does anything else depend on it being done?

If you can answer those four questions, you can almost always sketch the async code, regardless of the syntax in front of you.

Multi-file: a tiny event bus

Async thinking goes beyond fetching. Many systems use an event bus: parts of the program register interest ("call me when X happens"), and another part publishes events. This is callback-driven by design.

Code Block
JavaScript ES2023+

There's no Promise here — just callbacks. This is the same pattern the DOM uses (element.addEventListener), Node uses (EventEmitter), and many libraries use internally.

Challenge

Challenge
JavaScript ES2023+
Sequence callbacks

Write a function runSequentially(tasks, done) where:

  • tasks is an array of functions. Each function takes a single argument: a callback cb(err, result).
  • Your function should run them ONE AT A TIME (not in parallel), passing each result into an accumulator array.
  • When the last task finishes, call done(null, results) where results is an array of every result, in order.
  • If any task calls its callback with an error, call done(err) immediately and run no more tasks.

This is a small but realistic exercise in "callback orchestration".


QuestionSelect one

Why does JavaScript provide asynchronous APIs even though it runs on a single thread?

Because it makes code faster on multi-core CPUs

Because the language standard requires it

Because it lets the single thread stay busy on other work while waiting for slow operations, instead of blocking

Because asynchronous code is always easier to read

On this page