Skip to content
TaeyoungKim.dev

Why the Next Promise then Gets undefined Without return

WebWritten 2 min readTaeyoungKim
LinkedInX

If data is undefined in the second then after a fetch, inspect the first callback's return before blaming the network. A Promise chain passes each step's returned result to the next step.

Return the Promise inside then so the next step waits

The diagram compares a callback that calculates 6 but returns nothing with one that uses return value + 1 to pass 6 to the next then.

Start with the smallest case, adding one to five:

js
Promise.resolve(5)
  .then((value) => { return value + 1; })
  .then((value) => console.log(value)); // 6

Remove the first callback's return, and the next value is undefined. This propagation rule also applies to network responses.

js
const fakeResponse = new Response(JSON.stringify([{ id: 1, title: "Read" }]));

Promise.resolve(fakeResponse)
  .then((response) => {
    return response.json();
  })
  .then((todos) => {
    console.log(todos[0].title); // Read
  })
  .catch((error) => console.error(error));

The example works without a server in a browser console. response.json() returns a Promise. Returning it makes the next then wait for the parsed JSON. Omitting return makes the callback return undefined, so the next step has neither the waiting relationship nor the parsed data. Remove it and compare the failure at todos[0].

Braces make a missing return easy to overlook

An arrow function with a single expression returns it implicitly. With braces, write return explicitly:

js
// Without braces, the expression is returned implicitly.
Promise.resolve(new Response("{}")).then((value) => value.json());

// With braces, return the Promise explicitly.
Promise.resolve(new Response("{}")).then((value) => { return value.json(); });

The style matters less than being able to trace the value between stages. Packing conversion, rendering, and error handling into one callback can hide a missing return.

async/await follows the same result flow

js
async function loadTodos() {
  const response = await fetch("/api/todos");
  if (!response.ok) throw new Error("Request failed");
  return response.json();
}

await can make control flow easier to read, but it does not remove the need for failure handling. fetch does not normally reject merely because the server returned HTTP 404 or 500, so check response.ok. For a screen that may change while a request is pending, consider cancellation or stale-response handling with an AbortController.

Reproduce a missing return without a network request

js
Promise.resolve({ value: 3 })
  .then((result) => { result.value * 2; })
  .then((value) => console.assert(value === undefined));

Promise.resolve({ value: 3 })
  .then((result) => { return result.value * 2; })
  .then((value) => console.assert(value === 6));

The first chain calculates a number but does not return it. The next callback receives undefined; the second chain receives 6. For a real request, test HTTP status and JSON parse failures separately. A logging callback inserted into a chain also needs to pass on the value if the next step uses it.

Key takeaways

Promise chains use each then callback's return value for the next stage. Return asynchronous results such as response.json() so the next stage waits for and receives them. Check response.ok and handle errors separately; if the chain grows long, async/await can make the intended value flow clearer.

Author

TaeyoungKim

Connecting technical foundations with implementation, verification, and production decisions.

Read next