Instantly share code, notes, and snippets.

Embed
What would you like to do?
Processing errors with Fetch API

I really liked @tjvantoll article Handling Failed HTTP Responses With fetch(). The one thing I found annoying with it, though, is that response.statusText always returns the generic error message associated with the error code. Most APIs, however, will generally return some kind of useful, more human friendly message in the body.

Here's a modification that will capture this message. The key is that rather than throwing an error, you just throw the response and then process it in the catch block to extract the message in the body:

fetch("/api/foo")
  .then( response => {
    if (!response.ok) { throw response }
    return response.json()  //we only get here if there is no error
  })
  .then( json => {
    this.props.dispatch(doSomethingWithResult(json)) 
  })
  .catch( err => {
    err.text().then( errorMessage => {
      this.props.dispatch(displayTheError(errorMessage))
    })
  })

Frankly, I'm horrified that JavaScript let's you throw some random value, rather than an error, but hey, when in Rome...

@drinkwater99

This comment has been minimized.

drinkwater99 commented Sep 20, 2017

Cool! Exactly what I was looking for.

@kujma10

This comment has been minimized.

kujma10 commented Oct 4, 2017

This is not the optimal solution.

if (!response.ok) { throw response }

what if there is an error after this (not the server error) or what if the server is not available at all? then err.text() will throw err.text is not a function

It cannot be guaranteed that catch clause will be executed only because of throw response

@johnlim

This comment has been minimized.

johnlim commented Oct 5, 2017

@kujma10 Do you have any alternative as to how we extract the error message from the body then? Will this work?

 .catch( err => {
      if (typeof err.text === 'function') {
        err.text().then(errorMessage => {
          this.props.dispatch(displayTheError(errorMessage))
        });
      } else {
           console.log(err)
      }
    } 
@pandorasNox

This comment has been minimized.

pandorasNox commented Nov 28, 2017

Since .text() will work definitely for fetch promise resolve you should not (maybe never) work with .json() in the first place, you could try:

fetch("/api/foo")
    .then( response => {
        return response.text();
    })
    .then( responseBodyAsText => {
        try {
            const bodyAsJson = JSON.parse(responseBodyAsText);
            return bodyAsJson;
        } catch (e) {
            Promise.reject({body:responseBodyAsText, type:'unparsable'});
        }
    })
    .then( json => {
            this.props.dispatch(doSomethingWithResult(json)) 
    })
    .catch( err => {
        if (false === err instanceof Error &&  err.type && err.type === 'unparsable' {
            this.props.dispatch(displayTheError(err.body))
            return;
        }
        throw err;
    })
@danielgormly

This comment has been minimized.

danielgormly commented Dec 1, 2017

I'm trying to make a unified error handling solution in my app and came to a similar conclusion. I haven't tested it yet but here's my drawn out approach which differentiates error types so that my UI components can intelligently determine what error they need to display.

export const parseAPIResponse = response =>
	new Promise(resolve => resolve(response.text()))
		.catch(err =>
			// eslint-disable-next-line prefer-promise-reject-errors
			Promise.reject({
				type: 'NetworkError',
				status: response.status,
				message: err,
			}))
		.then((responseBody) => {
			// Attempt to parse JSON
			try {
				const parsedJSON = JSON.parse(responseBody);
				if (response.ok) return parsedJSON;
				if (response.status >= 500) {
					// eslint-disable-next-line prefer-promise-reject-errors
					return Promise.reject({
						type: 'ServerError',
						status: response.status,
						body: parsedJSON,
					});
				}
				if (response.status <= 501) {
					// eslint-disable-next-line prefer-promise-reject-errors
					return Promise.reject({
						type: 'ApplicationError',
						status: response.status,
						body: parsedJSON,
					});
				}
			} catch (e) {
				// We should never get these unless response is mangled
				// Or API is not properly implemented
				// eslint-disable-next-line prefer-promise-reject-errors
				return Promise.reject({
					type: 'InvalidJSON',
					status: response.status,
					body: responseBody,
				});
			}
		});
@aldo-jr

This comment has been minimized.

aldo-jr commented Dec 13, 2017

Why should we not use .json()? @pandorasNox

@Andsbf

This comment has been minimized.

Andsbf commented Jan 9, 2018

👏 really useful, good job guys!

@Andsbf

This comment has been minimized.

Andsbf commented Jan 9, 2018

this might be useful
github/fetch#203 (comment)

@Anubisss

This comment has been minimized.

Anubisss commented Mar 7, 2018

I solved this way:

handleSubmit(e) {
  e.preventDefault()

  const body = {
    email: this.state.email,
  }

  let resStatus = 0
  fetch(Config.REST_API_URL + 'users/registration-request', {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  })
  .then(res => {
    resStatus = res.status
    return res.json()
  })
  .then(res => {
    switch (resStatus) {
      case 201:
        console.log('success')
        break
      case 400:
        if (res.code === 'ValidationFailed') {
          // My custom error messages from the API.
          console.log(res.fieldMessages)
        } else {
          console.log('this is a client (probably invalid JSON) error, but also might be a server error (bad JSON parsing/validation)')
        }
        break
      case 500:
        console.log('server error, try again')
        break
      default:
        console.log('unhandled')
        break
    }
  })
  .catch(err => {
    console.error(err)
  })
}
@incorelabs

This comment has been minimized.

incorelabs commented Oct 22, 2018

Server Error Handling using Fetch

With some helpful insights from this article, https://css-tricks.com/using-fetch/#article-header-id-5,
I handle this issue with this syntax

function handleResponse(response) {
    return response.json()
        .then((json) => {
            if (!response.ok) {
                const error = Object.assign({}, json, {
                    status: response.status,
                    statusText: response.statusText,
                });

                return Promise.reject(error);
            }
            return json;
        });
}

function doSomethingWithTheResolvedJSON(json) {

        // With great power, comes great responsibility

        console.log(json);

        // :-P
}

fetch("/api/foo")
    .then(handleResponse)
    .then(doSomethingWithTheResolvedJSON)
    .catch(error => {

        // This error object will have the error from the server
        // As well as the two additions we made earlier of the status and statusText

        console.log(error);
    });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment