Javascript

Why does json return a promise but not when it passes through then

19 September 2026 · 9 min read

Why does json return a promise but not when it passes through then

Understanding asynchronous JavaScript can sometimes feel like navigating a maze. One common point of confusion arises when working with the .json() method in the Fetch API: Why does .json() return a promise directly, but seemingly doesn’t when you chain it inside a .then() block? This behavior stems from the asynchronous nature of network requests and the way promises are handled in JavaScript. Grasping this concept is crucial for effectively handling data fetched from APIs and building robust web applications. We’ll explore the underlying mechanics, providing clarity on how .json() and promises interact, and offer practical examples to solidify your understanding. We’ll also delve into potential pitfalls and best practices for working with asynchronous data in JavaScript, ensuring you can confidently handle API responses in your projects.

Understanding Asynchronous Operations and Promises

To fully grasp why .json() behaves as it does, it’s essential to understand asynchronous operations and how promises manage them. JavaScript is single-threaded, meaning it can only execute one operation at a time. When dealing with tasks that take time, such as fetching data from a server, synchronous execution would freeze the browser. Asynchronous operations allow the browser to continue running while waiting for the task to complete. Promises are objects that represent the eventual completion (or failure) of an asynchronous operation and its resulting value.

The Fetch API, which includes the .json() method, is inherently asynchronous. When you call fetch(), it returns a promise that resolves with a Response object. This Response object contains the headers and status of the response, but not necessarily the body itself. To extract the body as JSON, you call the .json() method on the Response object. Since parsing the response body can also take time, .json() itself returns a promise. This promise resolves with the parsed JSON data, allowing you to work with the data in your code.

Therefore, the asynchronous nature of network requests and the promise-based design of the Fetch API are the fundamental reasons behind the behavior observed with .json(). Without this asynchronous handling, web applications would become unresponsive during data retrieval, leading to a poor user experience. The Fetch API’s design ensures that the main thread remains free to handle user interactions and other tasks while data is being fetched and processed in the background. According to Mozilla documentation, using Fetch is now preferred over older methods like XMLHttpRequest due to its promise-based approach and cleaner syntax. Learn more about the Fetch API on MDN.

The Role of .then() in Promise Chaining

The .then() method is crucial for working with promises. It allows you to chain asynchronous operations together, ensuring that they execute in a specific order. When you chain .then() after a promise, it creates a new promise that resolves with the value returned by the function inside the .then() block. This is where the apparent disappearance of the promise from .json() within a .then() block comes into play.

When you call .json() directly, you’re explicitly seeing the promise it returns. However, when you chain it within a .then() block, the .then() method implicitly handles the promise returned by .json(). The function inside the .then() block doesn’t execute until the promise returned by .json() resolves. This is because .then() waits for the preceding promise to settle (either resolve or reject) before executing its callback function. Once the .json() promise resolves with the parsed JSON data, that data is passed as an argument to the function inside the .then() block. You’re then working directly with the JSON data, not the promise itself.

To illustrate, consider this snippet:

fetch('https://api.example.com/data') .then(response => response.json()) .then(data => { console.log(data); // 'data' is the resolved JSON object }); 

In this example, response.json() returns a promise, but the .then(data => ...) block only executes after that promise resolves. The variable data within that block holds the actual JSON data, not a promise. This seamless integration is a key feature of promises, allowing for cleaner and more readable asynchronous code. You can find additional examples and explanations of promise chaining on JavaScript.info. Practical Examples and Code Demonstrations

Let’s look at some practical examples to further clarify how .json() and .then() work together. We’ll start with a basic example and then explore more complex scenarios.

Example 1: Fetching and displaying data

Suppose you want to fetch user data from an API and display it on your webpage. Here’s how you can do it:

fetch('https://jsonplaceholder.typicode.com/users/1') .then(response => response.json()) .then(user => { document.getElementById('name').textContent = user.name; document.getElementById('email').textContent = user.email; }) .catch(error => console.error('Error fetching data:', error)); 

In this example, the first .then() block converts the response to JSON, and the second .then() block uses the parsed JSON data (the user object) to update the webpage. The .catch() block handles any errors that might occur during the process. This showcases the power and simplicity of promise chaining for handling asynchronous operations.

Infographic here explaining the promise lifecycle
Example 2: Handling multiple asynchronous requests

Sometimes, you need to make multiple API requests and process the results together. Promises make this easier with methods like Promise.all():

Promise.all([ fetch('https://jsonplaceholder.typicode.com/todos/1').then(response => response.json()), fetch('https://jsonplaceholder.typicode.com/posts/1').then(response => response.json()) ]) .then(([todo, post]) => { console.log('Todo:', todo); console.log('Post:', post); }) .catch(error => console.error('Error fetching data:', error)); 

Here, Promise.all() takes an array of promises and resolves with an array of the resolved values when all promises have resolved. This is useful when you need to wait for multiple asynchronous operations to complete before proceeding. Remember to handle potential rejections using .catch() to prevent unhandled promise rejections. Proper error handling is crucial for maintaining the stability and reliability of your application. The use of asynchronous functions improves the perceived performance of web applications. Best Practices and Common Pitfalls

While promises offer a powerful way to handle asynchronous operations, there are some best practices to keep in mind to avoid common pitfalls. Understanding these will help you write more robust and maintainable code.

  • Always handle errors: Use .catch() to handle potential errors in your promise chains. Unhandled promise rejections can lead to unexpected behavior and make debugging difficult.
  • Avoid nested .then() blocks: Nested .then() blocks can make your code harder to read and maintain. Use promise chaining or async/await to keep your code flat and readable.
  • Use async/await for cleaner syntax: async/await provides a more synchronous-looking syntax for working with promises. It can make your code easier to understand and reason about.

Another common pitfall is forgetting that .json() returns a promise. This can lead to errors if you try to access the JSON data before the promise has resolved. Always ensure that you’re working with the resolved data within a .then() block or using await in an async function. Failing to do so will lead to undefined results. Consider the example below:

async function fetchData() { const response = await fetch('https://jsonplaceholder.typicode.com/todos/1'); const jsonData = await response.json(); console.log(jsonData); } fetchData(); 

This example demonstrates how async/await can make asynchronous code easier to read and write. The await keyword pauses the execution of the function until the promise resolves, allowing you to work with the resolved data directly. According to a study by Google, adopting asynchronous practices like async/await can significantly improve page load times by preventing the main thread from blocking. Learn more about Asynchronous JavaScript on Web.dev. Proper usage of the Fetch API is essential for modern web development, allowing developers to efficiently handle network requests and create dynamic user interfaces.

Frequently Asked Questions (FAQ)

Why does `response.json()` return a promise?
Because parsing the response body into JSON is an asynchronous operation. It takes time, and the browser needs to remain responsive while it's happening. Therefore, `.json()` returns a promise that resolves with the parsed JSON data when it's ready.
What happens if the JSON parsing fails?
If the JSON parsing fails (e.g., the response body is not valid JSON), the promise returned by `.json()` will reject with an error. You should always handle potential errors using `.catch()`.
Can I use `async/await` instead of `.then()`?
Yes, `async/await` provides a more synchronous-looking syntax for working with promises. It can make your code easier to read and understand. However, you need to use it inside an `async` function.
How can I handle multiple API requests concurrently?
You can use `Promise.all()` to make multiple API requests concurrently. It takes an array of promises and resolves with an array of the resolved values when all promises have resolved.
To summarize, **.json() returns a promise** because it performs an asynchronous operation of parsing the response body. When chained within a `.then()` block, the promise is implicitly handled, providing you with the resolved JSON data. By understanding the asynchronous nature of JavaScript, the role of promises, and best practices for handling them, you can confidently manage API responses and build efficient web applications.
  1. Fetch data using the fetch() function.
  2. Convert the response to JSON using response.json().
  3. Handle the resolved JSON data in a .then() block.
  4. Catch and handle any errors using .catch().
  • Always handle errors using .catch().
  • Use async/await for cleaner syntax.

Hopefully, this explanation clarifies why .json() behaves as it does and empowers you to work more effectively with asynchronous JavaScript. Explore our other articles for more in-depth explanations of common programming challenges and best practices. Now that you understand how .json() and promises work, you can confidently build robust and responsive web applications that handle asynchronous data efficiently. Don’t hesitate to experiment with these concepts in your own projects and continue exploring the vast landscape of JavaScript development. By embracing these techniques, you’ll be well-equipped to tackle even the most complex asynchronous tasks.

Question & Answer :
I’ve been messing around with the fetch() api recently, and noticed something which was a bit quirky.

let url = "http://jsonplaceholder.typicode.com/posts/6"; let iterator = fetch(url); iterator .then(response => { return { data: response.json(), status: response.status } }) .then(post => document.write(post.data)); ; 

post.data returns a Promise object. http://jsbin.com/wofulo/2/edit?js,output

However if it is written as:

let url = "http://jsonplaceholder.typicode.com/posts/6"; let iterator = fetch(url); iterator .then(response => response.json()) .then(post => document.write(post.title)); ; 

post here is a standard Object which you can access the title attribute. http://jsbin.com/wofulo/edit?js,output

So my question is: why does response.json return a promise in an object literal, but return the value if just returned?

Why does response.json return a promise?

Because you receive the response as soon as all headers have arrived. Calling .json() gets you another promise for the body of the http response that is yet to be loaded. See also Why is the response object from JavaScript fetch API a promise?.

Why do I get the value if I return the promise from the then handler?

Because that’s how promises work. The ability to return promises from the callback and get them adopted is their most relevant feature, it makes them chainable without nesting.

You can use

fetch(url).then(response => response.json().then(data => ({ data: data, status: response.status }) ).then(res => { console.log(res.status, res.data.title) })); 

or any other of the approaches to access previous promise results in a .then() chain to get the response status after having awaited the json body. Modern version using await (inside an async function):

const response = await fetch(url); const data = await response.json(); console.log(response.status, data.title); 

Also, you might want to check the status (or just .ok) before reading the response, it might not be JSON at all.