TAKANORI HIDIKA

Notes hero

Jun 12, 2025

JavaScript

From Promises to Fetch: Building a Mental Model of Async JavaScript

From Promises to Fetch: Building a Mental Model of Async JavaScript thumbnail

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 in Promise.resolve(...)
    • catch handles errors
    • finally 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 a Response 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