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
fetch
are 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:
then
chains the next step; return values are automatically wrapped inPromise.resolve(...)
catch
handles errorsfinally
runs 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 aResponse
object- 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