Skip to content

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.

Copy link

drinkwater99 commented Sep 20, 2017

Cool! Exactly what I was looking for.

@kujma10-zz

This comment has been minimized.

Copy link

kujma10-zz 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.

Copy link

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.

Copy link

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.

Copy link

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.

Copy link

aldo-jr commented Dec 13, 2017

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

@Andsbf

This comment has been minimized.

Copy link

Andsbf commented Jan 9, 2018

👏 really useful, good job guys!

@Andsbf

This comment has been minimized.

Copy link

Andsbf commented Jan 9, 2018

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

@Anubisss

This comment has been minimized.

Copy link

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.

Copy link

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);
    });
@mpdroog

This comment has been minimized.

Copy link

mpdroog commented Mar 25, 2019

Original code with no is not a function solution.

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 => {
  if (err.text) {
    err.text().then( errorMessage => {
      this.props.dispatch(displayTheError(errorMessage))
    })
  } else {
    this.props.dispatch(displayTheError('Error.')) // Hardcoded error here
  }
})

Thanks for sharing your code! :)

@Popov85

This comment has been minimized.

Copy link

Popov85 commented Jun 19, 2019

Here is a variant of working code that is able to deal with both caught promises and Error objects
The actual error message is landed to error.message state value in a React.js component.

fetch("http://localhost:8080/test/error", {
            method: 'GET',
            headers: {
                'Accept': 'application/json'
            }
        })
            .then(result => {
                if (!result.ok) throw result;
                return result.json();
            })
            .then(result => {
                console.log(result);
                this.setState({
                    isLoaded: true,
                    error: null
                });
            }).catch(error => {
                console.log("Error occurred");
                try {
                    error.json().then(body => {
                        //Here is already the payload from API
                        console.log(body);
                        console.log("message = "+body.message);
                        this.setState({
                            isLoaded: true,
                            error: body
                        });
                    });
                } catch (e) {
                    console.log("Error parsing promise");
                    console.log(error);
                    this.setState({
                        isLoaded: true,
                        error: error
                    });
                } 
            });

P.S. It is sad that for this typical kind of job we need to invent a wheel, to sorrow of the new Fetch API(((

@uwuru

This comment has been minimized.

Copy link

uwuru commented Jul 20, 2019

I found this gist while working this out too. Here is how I have done it. My response is JSON and has useful information to add to the error message. Creates a new Error object so that else where in my code either type of error is presented/accessed the same (I reference error.message else where). Any questions just ask :)

return fetch(url)
    .then((response) => {
      if (response.ok) {
        return response.json()
      }
      throw response
    })
    .catch((error) => {
      if (error instanceof Error) {
        return { error }
      }

      return error.json().then((responseJson) => {
        return {
          error: new Error(
            `HTTP ${error.status} ${error.statusText}: ${responseJson.msg}`
          )
        }
      })
    })
@ghost

This comment has been minimized.

Copy link

ghost commented Nov 28, 2019

Based on @danielgormly answer I come up with the following

fetch(url, options)
    .then(response => {
        // reject not ok response
        if (!response.ok) {
            return Promise.reject(response)
        }
        return response.json() // or return response.text()
    })
    // catch error response and extract the error message
    .catch(async response => {
        const error = await response.text().then(text => text)
        return Promise.reject(error)
    })
    .then(data => {
        // you've got your data here
    })
    .catch(error => {
        // finally handle the error
    })
@sprankhub

This comment has been minimized.

Copy link

sprankhub commented Jan 13, 2020

I am really not a JS guy, but the following should work:

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

This comment has been minimized.

Copy link

Austinmac56 commented Mar 5, 2020

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

@othneildrew

This comment has been minimized.

Copy link

othneildrew commented Apr 1, 2020

This post has been really helpful guys!
Here's my take on a solution that works very well with various types of data. This is similar to @incorelabs, but with my own sick wicked twist.

So to give you some background, I created an API that handles errors and returns an error as an object as many APIs do.
Example error response from server:

{
    "error": "invalid_request",
    "error_description": "The request is missing a required parameter...",
    "hint": "Check the `client_id` parameter",
    "message": "The request is missing a required parameter...."
}

The problem with the other solutions is that I'm not able to throw and return the same error object, I can only reject and return strings that are not entirely helpful and really vague. So I decided to modify the response to first include the status code, text, etc. Here's the complete solution

function makeRequest(url, options) {
    return new Promise((resolve, reject) => {
        fetch(url, options)
            .then(handleResponse)
            .then(response => JSON.parse(response))
            .then((json) => resolve(json))
            .catch((error) => {
                try {
                    reject(JSON.parse(error))
                }
                    catch(e) {
                        reject(error)
                    }
            })
    })
}

function handleResponse(response) {
    return response.json()
        .then((json) => {
            // Modify response to include status ok, success, and status text
            let modifiedJson = {
                success: response.ok,
                status: response.status,
                statusText: response.statusText ? response.statusText : json.error || '',
                response: json
            }
            
            // If request failed, reject and return modified json string as error
            if (! modifiedJson.success) return Promise.reject(JSON.stringify(modifiedJson))

            // If successful, continue by returning modified json string
            return JSON.stringify(modifiedJson)
        })
}



// Then I use it in other files like so. First declare any options, if needed.
const options = {
// your options... method: POST, headers, cors, etc.
}

// Then make the request using the promise function
makeRequest(/oauth/token, options)
            .then((data) => console.log(data)) // do something great with data
            .catch(error => console.log(error)) // do something useful with error

The expected response will always be (Example):

{
                success: true, // or false
                status: 200, // or 400, 401, etc
                statusText: "",
                response: {
                         // original response from server
                }
}

I've tested this with various APIs and it works so far in all cases Also, I'm using fetch from the isomorphic-unfetch package in a NextJS application.

Hope this helps someone!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.