Get AI summaries of any video or article — Sign up free
The Async Await Episode I Promised thumbnail

The Async Await Episode I Promised

Fireship·
5 min read

Based on Fireship's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.

TL;DR

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?

Because the event loop prioritizes micro tasks over macro tasks. setTimeout schedules a macro task for the next event loop cycle, while a resolved promise schedules a micro task that runs before the next cycle begins. In the transcript’s example, synchronous logs run immediately, the timer callback waits for the next cycle, and the promise resolution executes first due to micro task queue priority.

What misconception about promises leads to UI or program freezes?

Wrapping code in a promise doesn’t automatically offload CPU work to another thread. The transcript shows a while loop that blocks for hundreds of milliseconds; even when placed inside a promise, the blocking work still happens on the main thread during synchronous execution. Only the promise resolution timing changes (micro task scheduling), not the fact that the loop blocks the thread.

How does async/await change the structure of promise-based code?

An async function automatically returns a promise. Using await pauses execution within that async function until the awaited promise resolves, letting developers write multi-step asynchronous logic with variables and linear control flow. This avoids long then chains and makes it easier to pass intermediate results (like fruit values) to later steps.

When should developers avoid awaiting promises sequentially?

When the awaited operations are independent. The transcript’s fruit example shows that awaiting pineapple and then strawberry adds roughly one full latency per await. Starting both operations at once and awaiting Promise.all runs them concurrently, cutting total time roughly in half (in the demo’s simulated latency scenario).

Why can async/await behave unexpectedly inside map or forEach?

Because those iteration helpers don’t “pause” the outer function the way a traditional loop does. The transcript warns that using await inside map/forEach can still kick off multiple promises concurrently, which may differ from the intended sequential behavior. For sequential awaiting, a classic for loop with await inside it is recommended.

How do try-catch and error propagation differ from promise chaining with catch?

With async/await, errors thrown by awaited promises can be handled in a surrounding try-catch block. The transcript notes that what happens next depends on the catch block: returning a value effectively replaces the failure with a fallback result (so the consumer may not see an error), while rethrowing breaks the consumer’s promise chain and routes the error to the consumer’s catch.

Review Questions

  1. In an event loop scenario with both setTimeout and a resolved promise, which runs first and why (macro vs micro task)?
  2. What’s the difference between sequential awaiting and Promise.all for independent async operations?
  3. Why doesn’t await inside map/forEach guarantee sequential execution, and when should a traditional for loop be used?

Key Points

  1. 1

    Understand the event loop’s macro-task vs micro-task priority, since it determines whether timers or promises resume first.

  2. 2

    Async/await is built on promises; async functions always return promises, and await pauses only within that async function.

  3. 3

    Wrapping code in a promise does not prevent main-thread blocking—CPU-heavy synchronous work still freezes execution.

  4. 4

    Use Promise.all (or similar batching) to run independent async operations concurrently and avoid accidental serialization.

  5. 5

    Prefer try-catch around awaited calls for clearer error handling, and decide whether to return fallback values or rethrow errors.

  6. 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.

Highlights

Micro tasks from resolved promises run before the next event loop cycle, so promises can beat setTimeout callbacks even if timers were queued first.
Promises don’t move CPU work off the main thread; blocking loops still halt execution during promise creation.
Async/await improves readability by turning then chains into linear code with awaitable variables.
Awaiting independent operations sequentially wastes latency; Promise.all restores concurrency.
Async/await inside map/forEach can unintentionally run everything concurrently—use a traditional for loop for true sequential control.

Topics

Mentioned

  • Jake Archibald
  • Stephen Fluence
  • ES 7