async/await Is Just Promises in Disguise
async/await isn't a new concurrency model. It's sugar on top of promises that lets you write code that looks sequential even though it's asynchronous. Same machinery, much friendlier shape.
Here's the same task written both ways:
Both functions return a promise. Both do exactly the same thing. The async version reads top-to-bottom with no .then chain — that's the whole appeal.
async Marks a Function as Returning a Promise
Put async in front of a function, an arrow function, or a method, and two things happen:
- The function always returns a promise. Whatever you
returnbecomes the resolved value. - You're allowed to use
awaitinside it.
Notice result isn't the string — it's a promise that resolves to the string. Even though greet contains no await and no async work, the async keyword still wraps the return value in a promise. If the function throws, the promise rejects.
await Pauses Until a Promise Settles
Inside an async function, await somePromise pauses that function until the promise resolves, then hands you the resolved value. If the promise rejects, await throws.
Watch the output order. "started countdown" prints before "2" — because await only pauses the async function, not the rest of the program. The event loop keeps running; countdown just resumes later when each wait promise resolves.
You can await anything promise-like. await 42 is legal too — non-promises get wrapped in Promise.resolve(42) and resolved immediately.
Error Handling With try/catch
With plain promises you chain .catch(). With async/await, a rejected promise becomes a thrown exception you can catch the normal way:
One try/catch covers every await inside it. Network failures, JSON parse errors, and your own throw statements all land in the same catch. That's a real upgrade over nested .then/.catch chains.
One thing to watch: fetch only rejects on network errors, not HTTP 4xx/5xx. You check res.ok yourself and throw — a pattern you'll see constantly in real code.
Don't await in a Loop When You Don't Have To
This is the most common async/await pitfall. Sequential await inside a loop means each iteration waits for the previous one:
sequential takes ~900ms. parallel takes ~300ms. The rule of thumb: if tasks don't depend on each other's results, start them all, then await Promise.all. Only await one-by-one when the next call actually needs the previous result.
For collections, Promise.all(items.map(async (x) => ...)) is the idiom. A plain for...of with await inside runs serially — sometimes you want that (rate-limiting, ordering) but usually you don't.
Mixing async/await and Plain Promises
You don't have to pick a side. async functions return promises, and await works on any promise — so you can mix them freely:
The two styles are interchangeable. Use await when the code reads better top-down; use .then when you want a quick one-off or you're working outside an async context.
Top-Level await (in ES Modules)
Historically, you had to wrap await in an async function because await wasn't allowed at the top level of a script. That changed: inside an ES module (a .mjs file or a <script type="module">), you can now await directly at the top:
// in an ES module
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
const user = await res.json();
console.log(user.name);
Top-level await delays the module's completion until the awaited promise settles — any importer of that module waits too. Handy for config loading and dynamic imports, but use it sparingly: a slow top-level await blocks everyone who imports the module.
In CommonJS files or regular inline scripts, this still fails with a SyntaxError. The classic workaround is an immediately-invoked async function:
Things That Trip People Up
A quick tour of the usual gotchas:
- Forgetting
async. Usingawaitin a regular function is a syntax error. The fix is addingasync— or calling the async helper with.then. - Forgetting to
awaitthe result.const data = getJSON(url);gives you a promise, not the data. If you use it as if it were the value, you'll see[object Promise]show up in your output. - Unhandled rejections. An async function you fire-and-forget (
doWork();) will silently swallow errors unless you add.catchorawaitit inside atry/catch. forEachwith async callbacks.array.forEach(async (x) => await something(x))doesn't wait for anything —forEachignores the returned promises. Usefor...ofwithawait, orPromise.all(array.map(...)).
Run it — "finished?" prints before any "done", because broken returns without waiting. fixed waits for everything, then prints "finished!" last.
When to Reach for async/await
Default to async/await for any code that does more than one asynchronous step in sequence, or that needs try/catch-style error handling. Stick with raw promises for trivial one-liners, for library code that returns a promise without needing to await anything itself, or when you genuinely need combinators like Promise.race or .finally() in a chain.
Used well, async/await makes async code read like a recipe: do this, then this, then this. The event loop still does its thing — you just get to stop thinking in callbacks.
Next: The fetch API
Most of the examples here used fetch as a stand-in for "some async thing." It's worth a proper look — how requests and responses work, handling JSON, setting headers, and why fetch doesn't reject on HTTP errors. That's the next page.
Frequently Asked Questions
What does async/await do in JavaScript?
async/await is syntax for working with promises that lets you write asynchronous code as if it were synchronous. async marks a function as returning a promise, and await pauses inside that function until a given promise settles, then gives you the resolved value. Underneath, it's still promises — just easier to read.
Can I use await outside an async function?
At the top level of an ES module, yes — that's called top-level await. Inside regular functions or CommonJS scripts, no: await is a syntax error outside an async function. The fix is usually to wrap the code in an async function and call it, or switch the file to an ES module.
How do I handle errors with async/await?
Wrap the awaited calls in try/catch. Any rejected promise you await turns into a thrown exception the catch block can handle. For background tasks you don't await, attach a .catch() to the returned promise so rejections don't go unhandled.
Does await block the whole program?
No. await only pauses the current async function. The event loop keeps running — timers fire, other async tasks progress, the UI stays responsive. The calling code gets a pending promise back and continues immediately.