Jun 12, 2025
JavaScript
From Promises to Fetch: Building a Mental Model of Async JavaScript
Overview
While learning how to write test codes for API Routes, I found myself confused by the layered asynchronous behavior of the fetch() function. At first, I thought fetch() immediately gave me the actual data, but I soon realized it returns a Promise that wraps a Response object—and even that object doesn't contain the actual data yet.
To fully understand what's going on, I revisited the fundamentals of asynchronous processing in JavaScript: how Promises work, what async/await really does, and how the browser or Node.js environment handles async execution. This deeper understanding helped me build a clear mental model for how data flows through async operations, especially when dealing with APIs.
After this learning, I noticed that when reviewing my notes on API Routes or Server Actions in Next.js, I could suddenly "see" the asynchronous behavior and fetch flow very clearly. The abstract concepts had finally clicked.
What I Learned
Concept of Asynchronous Execution
- JavaScript runs in a single-threaded environment
- Asynchronous APIs like
fetchare provided by the browser or Node.js, and offload work to external threads -
The async flow is managed via:
- Execution Context
- Call Stack
- Task Queue / Microtask Queue
- Event Loop
Promise
- Solves the problem of controlling execution order when waiting for async results
- Represents a pipeline of operations: result →
then→ next result - Has 3 states: pending, fulfilled, and rejected
-
Key methods:
thenchains the next step; return values are automatically wrapped inPromise.resolve(...)catchhandles errorsfinallyruns regardless of success or failure
-
Supports parallel async flows using:
Promise.all,Promise.race,Promise.any,Promise.allSettled,Promise.resolve,Promise.reject
async / await
- Syntactic sugar for Promises
- Allows writing async code that reads like synchronous code
- Makes complex async flows more readable and easier to debug
fetch()
fetch(url)returns a Promise that resolves to aResponseobject- That Promise is resolved once the HTTP headers are received
- However, the body (actual data) is streamed and not yet available at that point
res.json() / res.text()
- These methods are also asynchronous, returning Promises
- They read and parse the streamed body content (e.g. JSON or text)
-
This is why
fetch()is "double async":
const res = await fetch("/api/data"); // Wait for headers & Response object const data = await res.json(); // Wait for streamed body to be fully read and parsed
