
I'm going to admit something embarrassing for someone who writes JavaScript for a living: for years, I didn't actually understand async programming. I used it. I copied patterns that worked. I sprinkled await until the errors went away. But if you'd asked me to explain what was really happening, I'd have given you a confident, hand-wavy answer that didn't hold up.
Then one analogy made the whole thing click, permanently, in about ten minutes. Not a course. Not the docs. One way of seeing it.
If async still feels like a thing you survive rather than understand, this is the explanation I wish someone had given me five years earlier.
Async programming clicked for me when I stopped thinking about threads and timing and started thinking about it as ordering at a coffee shop. You place an order (start an async task), get a ticket (a promise), and step aside so others can order too — instead of blocking the whole line while your drink is made. await is just you choosing to wait at the counter for your ticket before doing the next thing. The runtime is the barista calling out names. Once that picture is in your head, the syntax stops being magic.
The reason I didn't understand async for years is that every explanation I found started in the wrong place. They started with mechanism — event loops, call stacks, microtask queues, threads versus non-threads.
All true. All useless to a confused beginner. You can't appreciate the mechanism until you understand the purpose, and everyone skipped the purpose.
So I want to start where they didn't: with why async exists at all. It exists to solve one annoying problem — waiting.
Photo by Ilya Pavlov on Unsplash
Imagine your code needs to fetch some data from a server. That takes time — maybe 200 milliseconds, maybe two seconds. In computer terms, an eternity.
The naive approach: your program stops everything and stares at the network connection until the data arrives. Nothing else happens. The whole app freezes. This is synchronous, blocking behavior, and for anything involving waiting — network, files, timers — it's a disaster. Your interface locks up while it waits.
Async exists to answer one question: how do we wait for slow things without freezing everything else?
That's it. That's the entire purpose. Everything else is mechanism in service of that goal. Hold onto the purpose and the rest follows. If you want the rigorous version of this picture once the analogy settles, the MDN Web Docs guides on promises and the event loop are the reference I send people to.
Here's the analogy that fixed my brain.
Picture a coffee shop with one cashier. The synchronous version works like this: you order a latte, and then everyone in line — and the cashier — stands frozen, doing nothing, until your latte is fully made and handed to you. Only then can the next person order. Absurd, right? The whole shop blocks on one drink.
Nobody runs a coffee shop that way. The async version is what real shops do:
The line keeps moving. Nobody's frozen. Multiple drinks get made "at once" without anyone blocking. That's async.
A promise isn't the coffee. It's the ticket that says the coffee is coming. Confusing the two is why async felt like magic for years.
That single distinction — the promise is the ticket, not the drink — was the thing that unlocked everything else for me.
await, really?Once the coffee shop is in your head, await becomes obvious.
await is you deciding: I'm going to stand at the counter and wait for my specific ticket to be called before I do my next thing. You're not freezing the whole shop — other people still order, the world keeps turning. You personally are just choosing to wait for your drink before moving on.
That's why await only makes sense inside an async function. You can only "wait at the counter" if you're a customer who's placed an order. Here's the shape in plain code:
async function getCoffee() {
console.log("placing order");
const coffee = await brewCoffee(); // wait for MY ticket
console.log("got it:", coffee); // runs after, only for me
}
console.log("shop keeps running"); // this doesn't wait!
The last line runs immediately, because the shop never froze. Only the stuff after the await, inside that function, waits. That used to baffle me. Now it's just obvious.
Photo by Priscilla Du Preez on Unsplash
After the model settled in, a bunch of bugs I used to make just… stopped happening, because I finally understood why they were bugs.
| Mistake | What's really happening | The fix |
|---|---|---|
Forgetting await | You took the ticket, not the coffee | await it before using |
await in a loop, one by one | Ordering one drink, fully waiting, then ordering the next | Fire them together, wait for all |
Trying to await in normal code | You're not a customer in line | Be inside an async function |
| Treating a promise like the value | Reading the ticket as if it's the drink | It resolves to the value later |
Every one of those used to be a mysterious error I'd patch by flailing. Now each is just "oh, I confused the ticket and the coffee again." The mental model turns random bugs into obvious ones.
Quick note for the present moment. AI coding tools will happily write async code for you, and it'll usually work. Tempting to never learn it.
Don't take that bait. Async bugs are some of the sneakiest — they pass tests, work locally, and break under real load or weird timing. If you don't understand the model, you can't debug what the AI handed you when it goes subtly wrong. Use the developer tools to write it faster, sure. But understand the coffee shop yourself, so you can fix it when a drink gets handed to the wrong person. This is the exact category of subtle, confidence-inspiring bug I kept running into when I coded with only AI for two weeks.
Once the coffee shop clicked, even error handling stopped scaring me. Because of course things go wrong in a coffee shop. The machine breaks. They're out of oat milk. Your name gets called and the drink is wrong.
In async terms, that's a promise rejecting instead of resolving. Your ticket comes back with bad news instead of a coffee. And just like in a real shop, you need a plan for "what do I do if my order fails?" — that's your try/catch around the await, or a .catch() on the promise.
async function getCoffee() {
try {
const coffee = await brewCoffee(); // wait for my ticket
console.log("enjoy:", coffee);
} catch (err) {
console.log("order failed:", err); // the machine broke
}
}
Before the analogy, error handling in async code felt like arbitrary ceremony I copied without understanding. After it, it's obvious: you're just deciding what happens when your order can't be filled. Every part of the syntax maps to something a real shop deals with every day.
Once you see a promise as a ticket, a rejected promise is just an order that couldn't be filled. Suddenly
try/catchisn't ceremony — it's a plan B.
I've since seen people explain async with restaurants, mail, phone calls, all sorts. The coffee shop works best for me for one reason: it makes the ticket the hero of the story.
Most explanations focus on the timing — when things run, the event loop, the queue. Useful eventually, but it buries the single insight that actually unlocks understanding: the promise is a placeholder for a future value, not the value itself. The ticket. Every confusion I ever had about async traced back to me mentally treating the ticket as the coffee — awaiting things that weren't promises, forgetting to await, reading a pending promise as if it held data.
Lead with the ticket, and the timing details slot in naturally afterward because you finally have something solid to attach them to. Lead with the timing, and you're memorizing mechanism for a purpose you don't yet grasp. Purpose first. Always purpose first.
Finding the right picture for a hard concept is itself a senior skill, and it's a thread that runs through the brutal truth about becoming a senior developer if you want to keep building that kind of understanding.
Q: Is async the same as multithreading? No, and this trips everyone up. JavaScript async is mostly single-threaded — one cashier, juggling tickets cleverly, not many cashiers working in parallel. It's about not blocking while waiting, not about doing many CPU things truly at once.
Q: When should I run async tasks together instead of one at a time? When they don't depend on each other. If you're fetching five independent things, order all five drinks at once and wait for all of them, rather than ordering, fully waiting, ordering the next. Independent waits should overlap.
Q: Why does my await seem to "do nothing"?
Usually you forgot it, or you're awaiting something that isn't actually a promise. You took a ticket and tried to drink it. Check that the thing you're awaiting really returns a promise.
Q: Do other languages work the same way? The promise/ticket idea shows up across many languages, with different names — futures, tasks, coroutines. The coffee shop analogy travels surprisingly well. The syntax changes; the purpose of not blocking on slow work does not.
I faked async for years because every explanation handed me the mechanism before the meaning. The fix was one picture: a coffee shop where you get a ticket instead of freezing the line, and await is just you waiting for your own name to be called.
Async isn't about threads or clever syntax. It's about not standing frozen while slow things happen. Get that, and the rest is detail.
So if async has quietly intimidated you too: does the coffee shop make it click? And what's the next concept you've been faking that just needs the right picture?
No following, no network, no luck. Just an unglamorous system I ran for eighteen months. Here's exactly what I did.

I went from 200 to 11,000 subscribers without hiring anyone. AI didn't write my newsletter — it did everything around it.

I chased big, audacious goals for years and burned out every time. Then I built my whole life around wins so small they felt like cheating.

Comments
Sign in to join the conversation
No comments yet. Be the first to share your thoughts!