Asynchronous code shouldn't be confusing. Async/Await provides the most elegant syntax for managing Promises β writing flat, linear code that reads like synchronous logic while remaining fully non-blocking.
1The async Keyword β Always Returns a Promise
The async keyword before a function declaration or expression makes it asynchronous. The critical rule: an async function always returns a Promise. If you return a string, it becomes Promise.resolve('string'). If you throw, it becomes a rejected Promise.
2The await Keyword β Pause, Unwrap, Continue
await can only be used inside an async function (or at the top level of an ES module). It pauses the function's execution until the Promise resolves, then unwraps the Promise and returns the resolved value. Crucially, await only pauses its own function β the main thread and other code continue executing.
3Error Handling with try/catch/finally
Instead of .catch() callbacks, async/await uses standard try...catch blocks. If any awaited Promise rejects, execution jumps to the catch block. You can also check res.ok and throw custom errors for non-200 HTTP responses. The finally block runs regardless of success or failure.
4Serial vs Parallel β The Performance Trap
Awaiting tasks one after another creates a serial execution pattern: each waits for the previous to finish. For independent tasks, this wastes time. Promise.all() fires all Promises simultaneously and waits for all to resolve β running in parallel. Two 2-second requests take 4 seconds serial, but only 2 seconds with Promise.all.
5Promise Combinators: all, allSettled, race, any
Promise.all: resolves when ALL succeed, rejects immediately if ANY fails (fail-fast). Promise.allSettled: waits for ALL regardless of success/failure β each result has a status of 'fulfilled' or 'rejected'. Promise.race: resolves or rejects with the FIRST Promise to finish. Promise.any: resolves with the FIRST success, ignoring rejections (throws AggregateError only if ALL fail).
6Top-Level Await in ES Modules
In ES modules (type="module" in browsers, .mjs in Node.js), you can use await at the top level without wrapping it in an async function. This simplifies module initialization: config loading, dynamic imports, and database connections can all use direct await.
