Intuition
A cook with three pans doesn’t stand and stare at the pasta water until it boils. They start the water, start the sauce, chop while both heat up, and go back to whichever is ready first. One cook, many dishes in progress, and nobody standing idle.
An async program is that cook. await means “this will take a while; come back to me when it’s ready”. Meanwhile the one thread is free to run something else.
How it works
An event loop runs one piece of code at a time. When a function reaches await on something that isn’t ready yet, it pauses: its place is saved, it leaves the call stack, and the loop runs other ready code. When the awaited thing finishes (a timer fires, a response arrives), the paused function joins the ready queue and later resumes exactly where it stopped.
In the figure:
maincallsfetchUser('A'), which runs untilawait sleep(...)and pauses.- The same happens for B, so both waits are now running at the same time.
mainawaitsPromise.all([a, b])and pauses. The stack is empty, but the program isn’t stuck: the loop is waiting for a timer.- Each timer fires, its function resumes and returns, and finally
mainresumes with both results.
The total time is the longest wait, not the sum. That’s the whole point.
Async is not parallel
Async lets one thread wait for many things at once. It doesn’t make computation faster: if a function adds up a billion numbers without awaiting, nothing else runs until it finishes. For CPU-heavy work you need threads or processes (Web Workers in the browser, multiprocessing in Python).
| Aspect | Async and await | Threads |
|---|---|---|
| Runs code in parallel | No: one thing at a time | Yes, on multiple cores |
| Switches tasks | Only at await |
At any moment |
| Best for | Many slow waits (requests, files) | Heavy computation, or blocking libraries |
| Shared data | Safe between awaits; can still race across one | Needs locks |
In each language
| Language | What it looks like |
|---|---|
| JavaScript | async functions return Promises. The browser and Node.js run one event loop; Promise.all waits for several at once. |
| Python | async def and await with asyncio. asyncio.gather or TaskGroup runs coroutines concurrently. A plain time.sleep inside async code blocks the whole loop. |
| Go | No async keyword: start a goroutine with go f(). The runtime parks a goroutine that waits, so ordinary blocking code is fine. |
| Rust | async fn returns a Future that does nothing until awaited. You need a runtime crate such as Tokio to run futures. |
| C# | Introduced async/await in 2012; Task.WhenAll waits for several tasks. |
| Java | No await; CompletableFuture, or virtual threads (Java 21) that let blocking code scale like async code. |
Common mistakes
- Awaiting in a loop when the waits are independent.
for (const id of ids) await fetch(id)waits for each one before starting the next. Start them all, thenawait Promise.all(...). - Forgetting
await. You get a Promise or coroutine object instead of the result, and errors go unnoticed. - Blocking the loop. A slow synchronous call (heavy computation,
time.sleep, a blocking database driver) freezes every other task. - Assuming no races. Between a read and a write separated by an
await, other tasks can change the data. See race conditions.
Check yourself
Check yourself
Two independent requests take 300 ms and 200 ms. Started together and awaited with Promise.all, how long until both are done?
The waits overlap, so the total is the longer of the two. Awaiting them one after the other would take 500 ms.
Check yourself
An async function adds up a billion numbers without any await. What happens to other tasks?
Tasks only switch at await. A long computation keeps the single thread busy, so nothing else runs until it’s done.
Sources
- MDN Web Docs, “Using promises” and “The event loop”.
- Python documentation, “Coroutines and Tasks” (
asyncio). - Asynchronous Programming in Rust (the Rust async book), chapter 1.
Finished this page?
Your list