fetch: How to get the response json on HTTP error 400+ ?

Suppose I create a request to http://example.com/404, and the response is 404 status code with a json response like this:

{
  "type": "error",
  "message": "What you were looking for isn't here."
}

How can I get the above json using fetch?

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Comments: 15 (2 by maintainers)

Most upvoted comments

Here is the code similar to @lasergoat, but handles json regardless of the response.status. Also in case of network error, rejects with a custom error object


/**
 * Parses the JSON returned by a network request
 *
 * @param  {object} response A response from a network request
 *
 * @return {object}          The parsed JSON, status from the response
 */
function parseJSON(response) {
  return new Promise((resolve) => response.json()
    .then((json) => resolve({
      status: response.status,
      ok: response.ok,
      json,
    })));
}

/**
 * Requests a URL, returning a promise
 *
 * @param  {string} url       The URL we want to request
 * @param  {object} [options] The options we want to pass to "fetch"
 *
 * @return {Promise}           The request promise
 */
export default function request(url, options) {
  return new Promise((resolve, reject) => {
    fetch(endpoint  + url, options)
      .then(parseJSON)
      .then((response) => {
        if (response.ok) {
          return resolve(response.json);
        }
        // extract the error from the server's json
        return reject(response.json.meta.error);
      })
      .catch((error) => reject({
        networkError: error.message,
      }));
  });
}

Hey, just want to let you know, I settled on a fair way of doing what I needed. I thought I’d post just in case anyone else ends up in a situation similar to me where they couldn’t use a catch because fetch gives a ReadableByteStream instead of a json object.

// filename: api.js

export default function api(uri, options = {}) {

  options.headers = {
    'Accept': 'application/json',
    'Authorization': `Bearer ${token}`
  };

  return new Promise((resolve, reject) => {

    fetch(endpoint + uri, options)

      .then(response => {

        if (response.status === 200) {

          return resolve(response.json());

        } else {

          return reject(response.status, response.json());

        }
      });
  });

}

This solution keeps your code clean and semantic. You use it like this:

    api(`user/${id}`).then(result => {
      console.log('success');
      dispatch(receiveUser(result))
    })
    .catch((status, err) => {
      console.log('err');
      console.log(err);
    });
fetch('/resource').then(function(response) {
  if (response.status === 404 || response.status === 200) {
    return response.json()
  }
}).then(function(object) {
  if (object.type === 'error') {
    console.log(object.type, object.message)
  } else {
    console.log('success')
  }
})

or

fetch('/resource').then(function(response) {
  if (response.status === 404) {
    response.json().then(function(object) {
      console.log(object.type, object.message)
    })
  } else if (response.status === 200) {
    response.json().then(function(object) {
      console.log('success')
    })
  }
})
fetch('/404').then(function(response) {
  if (response.status === 404) {
    return response.json()
  }
}).then(function(object) {
  console.log(object.type, object.message)
})

How would you catch both a 200 OR a 422 from a resource, both containing valid JSON in the body?

This is the only way I’ve figured it out so far.


    fetch(`${endpoint}user/${record.id}/`, {
      method: 'put',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'Authorization': `Bearer ${auth}`
      },
      body: JSON.stringify(record)
    })
    .then((result) => result.json())
    .then((result) => {

        if (result.id) {
          dispatch(savedUser( result ));
        } else {
          dispatch(savingUserError( result ));
        }

    });

To produce generic error objects I create a wrapper around the response and reject the promise. Finally throw the wrapped object. This produces a clean response on which higher level functions can depend.

fetch(url, opts)
      .catch(handleError) // handle network issues
      .then(checkStatus)
      .then(parseJSON)
      .catch(error => {
        throw error;
      });

const checkStatus = response => {
    if (response.status >= 200 && response.status < 300) {
      return response;
    }

    return response.json().then(json => {
      return Promise.reject({
        status: response.status,
        ok: false,
        statusText: response.statusText,
        body: json
      });
    });
  };

const parseJSON = response => {
    if (response.status === 204 || response.status === 205) {
      return null;
    }
    return response.json();
  };

const handleError = error => {
    error.response = {
      status: 0,
      statusText:
        "Cannot connect. Please make sure you are connected to internet."
    };
    throw error;
  };

use it like:

function* getUrl() {
    try {
        const response = yield fetch('some-url')
        // do something with response
    } catch (err) {
       // err is of type {status /*number*/, ok /*boolean*/, statusText /*string*/, body /*object*/}
   }
}

Instead of depending on the body (JSON) to contain an indicator I think it’s nicer to only look at the status. Downside is having to pass multiple arguments.

fetch('url')
  .then(response => Promise.all([response.ok, response.json()]))
  .then(([responseOk, body]) => {
    if (responseOk) {
      // handle success case
    } else {
      throw new Error(body);
    }
  })
  .catch(error => {
    // catches error case and if fetch itself rejects
  });

I ended up doing something like this. Probably not best but definitelly one of the shortes solutions. I just did not want to look at all those nested promises.

const parseFetchResponse = response => response.json().then(text => ({
  json: text,
  meta: response,
}))

return fetch(...your stuff...)
  .then(parseFetchResponse)
  .then(({ json, meta }) => {
    console.log('(response, meta)')
    console.log(json)
    console.log(meta)
})

@Siilwyn Agree, I always use response.ok and response.status to determine next action. Unfortunately, consider the fact that you might also want to return an error status with an error message as a JSON response…

{ error: {...}, ...}

Nice @Siilwyn but I had some responses as blobs and others as jsons so I have to check headers first, because in your promise, it always returns a json.

handlePromise = (response) => {
  if (response.headers) {
      const contentType = response.headers.get('Content-Type');
      if (contentType.includes('application/pdf')) {
        return Promise.all([response.ok, response.blob()]);
      }
      if (contentType.includes('application/json')) {
        return Promise.all([response.ok, response.json()]);
      }
    }
}

And use it on the fetch

fetch('url')
  .then(this.handlePromise)
  .then(([responseOk, body]) => { //body could be a blob, or json, or whatever!
    if (responseOk) {
      // handle success case
    } else {
      throw new Error(body);
    }
  })
  .catch(error => {
    // catches error case and if fetch itself rejects
  });

Just came to thank you guys!

I’m not that familiar with Javascript and spent way too much time with this but I ended up with a solution that was similar to, but not quite as beautiful as, the solution provided by @Siilwyn. I actually stumbled upon this page quite early in my research but didn’t know enough Javascript to recognize the solution staring in my face. So I’m going to take the time to explain it for future generations. 😉

The way I see it any call to fetch will result in one of the following four outcomes:

  1. Network error
  2. Parsing error (Unreadable JSON from the server)
  3. Business logic error (Readable JSON error message from the server)
  4. Success

In order for our application to be robust it must be able to deal with all these outcomes. The original question stems from the problem that the response and json objects aren’t available in the same context. Use Promise.all() to fix that.

Network- and parsing errors causes exceptions and it would be nice to unify our error handling. Throw an Error when you get a business logic error message from the server. That way all types of errors end up in the catch-method.

Finally I’m guessing the original question stems from a desire to provide good error messages. Business logic error messages will be provided by the server but the network- and parsing error messages must be provided by us. In the example below I’ve created a map with the Exception type as the key and I’m using exception.constructor to do the lookup. Note that network- and parsing error messages are provided in place and that business logic error messages are grabbed from the exception which in turn was created using the message provided by the server.

fetch(URL, OPTS)
    .then(response => Promise.all([response, response.json()]))
    .then(([response, json]) => {
        if (!response.ok) {
            throw new Error(json.message);
        }

        render(json);
    })
    .catch(exception => {
        console.log(new Map([
            [TypeError, "There was a problem fetching the response."],
            [SyntaxError, "There was a problem parsing the response."],
            [Error, exception.message]]).get(exception.constructor));
    });

Welp, here’s mine:

function processResponse(response) {
  return new Promise((resolve, reject) => {
    // will resolve or reject depending on status, will pass both "status" and "data" in either case
    let func;
    response.status < 400 ? func = resolve : func = reject;
    response.json().then(data => func({'status': response.status, 'data': data}));
  });
}

fetch('/some/url/')
      .then(processResponse)
      .then(response => {
        // repsonses with status < 400 get resolved. you can access response.status and response.data here
      })
      .catch(response => {
        // repsonses with status >= 400 get rejected. you can access response.status and response.data here too
        if (response.status === 400) {
            // handle form validation errors, response.data.errors...
        } else if (response.status === 403) {
            // handle permission errors
        } // etc
      });

Just make sure to always return a JSON parsable body in your responses and you should be good!