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:
Promise.resolve(5)
.then((value) => { return value + 1; })
.then((value) => console.log(value)); // 6Remove the first callback's return, and the next value is undefined. This propagation rule also applies to network responses.
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:
// 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
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
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.

