The Async Await Episode I Promised
Based on Fireship's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.
Understand the event loop’s macro-task vs micro-task priority, since it determines whether timers or promises resume first.
Briefing
Async/await matters because it turns JavaScript’s inherently asynchronous behavior—driven by the event loop—into code that reads like straight-line logic, without losing the performance benefits of non-blocking execution. The core idea starts with how browsers and Node.js run a single-threaded event loop: synchronous code runs immediately, while asynchronous work gets queued and later resumed. Crucially, the event loop distinguishes between macro tasks (like setTimeout/setInterval) and micro tasks (like fulfilled promises). Micro tasks run before the next event loop cycle begins, which can reorder execution in ways that surprise people who expect “line by line” behavior.
A simple example makes the point: console.log runs first, setTimeout(0) gets queued for a later macro task, a promise resolution is queued into the micro task queue, and then the final synchronous console.log runs. Even when the setTimeout callback was queued earlier, the promise’s micro task executes first because it has higher priority. That priority model explains why promises feel “faster” than timers and why understanding the event loop is the foundation for using async/await correctly.
From there, the transcript walks through promises as the underlying mechanism. APIs like fetch return promises, and promise chaining lets results flow through steps such as mapping a response to JSON. A single catch at the end can handle errors anywhere in the chain, avoiding the “one error handler per callback” problem common in callback-based code.
The lesson then targets a frequent misconception: wrapping heavy work in a promise doesn’t automatically move it off the main thread. Creating a promise and resolving it later still involves synchronous execution during promise creation; a CPU-blocking loop can freeze the program regardless. To avoid blocking, the expensive work must be structured so that synchronous execution completes first, and only then does the promise resolve—demonstrated by moving the loop into a resolved promise callback so the initial synchronous logs happen immediately.
With that groundwork, async/await is presented as syntactic sugar over promises. Marking a function as async makes it return a promise; using await pauses that function until the awaited promise resolves, letting developers write multi-step asynchronous flows without deep then chains. A fruit-and-smoothie example shows how values can be stored in variables after awaiting, making it easier to share results across steps.
The most important performance warning follows: awaiting sequentially can accidentally serialize independent operations. If two promises don’t depend on each other, awaiting them one after the other adds unnecessary latency. The fix is to start them concurrently (e.g., by collecting promises and using Promise.all) and then await the combined result. Error handling also becomes cleaner: try-catch around awaited calls replaces chained catch logic, with control flow determined by whether errors are rethrown or converted into fallback return values.
Finally, the transcript highlights practical patterns and pitfalls: async/await inside map or forEach doesn’t behave like a “pause-and-iterate” loop, so concurrency may happen unintentionally. When sequential behavior is required, a traditional for loop with await inside it is the right tool; when concurrency is desired, Promise.all and even for-await-of patterns can keep code concise. The takeaway is straightforward: async/await improves readability, but performance and correctness still depend on event loop timing, concurrency choices, and loop semantics.
Cornell Notes
JavaScript’s single-threaded event loop runs synchronous code immediately, then resumes queued work later. Macro tasks (e.g., setTimeout) run on the next event loop cycle, while micro tasks (e.g., resolved promises) run before the next cycle—so promise callbacks can jump ahead of timers. Promises enable chaining results and centralized error handling, but they don’t magically move CPU-heavy work off the main thread; blocking code still freezes execution during synchronous promise creation. Async/await is syntactic sugar over promises: async functions return promises, and await pauses within the function until the awaited promise resolves. The biggest practical risk is accidental serialization—use Promise.all for independent operations—and use try-catch for error handling and for correct loop behavior (for loops for sequential awaits, Promise.all for concurrency).
Why can a fulfilled promise run before a setTimeout(0) callback even when setTimeout was queued first?
What misconception about promises leads to UI or program freezes?
How does async/await change the structure of promise-based code?
When should developers avoid awaiting promises sequentially?
Why can async/await behave unexpectedly inside map or forEach?
How do try-catch and error propagation differ from promise chaining with catch?
Review Questions
- In an event loop scenario with both setTimeout and a resolved promise, which runs first and why (macro vs micro task)?
- What’s the difference between sequential awaiting and Promise.all for independent async operations?
- Why doesn’t await inside map/forEach guarantee sequential execution, and when should a traditional for loop be used?
Key Points
- 1
Understand the event loop’s macro-task vs micro-task priority, since it determines whether timers or promises resume first.
- 2
Async/await is built on promises; async functions always return promises, and await pauses only within that async function.
- 3
Wrapping code in a promise does not prevent main-thread blocking—CPU-heavy synchronous work still freezes execution.
- 4
Use Promise.all (or similar batching) to run independent async operations concurrently and avoid accidental serialization.
- 5
Prefer try-catch around awaited calls for clearer error handling, and decide whether to return fallback values or rethrow errors.
- 6
Choose loop patterns intentionally: use a traditional for loop for sequential awaits; use Promise.all/for-await-of when concurrency or streaming behavior is desired.