Skip to content

Instantly share code, notes, and snippets.

@odewahn
Last active February 27, 2024 09:56
Show Gist options
  • Save odewahn/5a5eeb23279eed6a80d7798fdb47fe91 to your computer and use it in GitHub Desktop.
Save odewahn/5a5eeb23279eed6a80d7798fdb47fe91 to your computer and use it in GitHub Desktop.
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
Copy link

Cool! Exactly what I was looking for.

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

@aldo-jr
Copy link

aldo-jr commented Dec 13, 2017

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

@Andsbf
Copy link

Andsbf commented Jan 9, 2018

👏 really useful, good job guys!

@Andsbf
Copy link

Andsbf commented Jan 9, 2018

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

@Anubisss
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
Copy link

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
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
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
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}`
          )
        }
      })
    })

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
Copy link

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
Copy link

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
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!

@Derimos
Copy link

Derimos commented Apr 10, 2020

Nice post, tnx :)
If you want to use message from API as error message.
In my case:
Api will return 400 in case of invalid data sent in request. Response body will contain: {"message":"Some nasty error message!"}. I want to use this message as error. But there is a problem if you want to reject promise. Because Promise.reject will reject immediately, and will not wait for resolving of result.json() for example.

Here is my take if you want to wait for result.json() and then use resulted json data as error.

fetch()
.then((response) => {
    if (response.status === 200) { // Or what ever you want to check
        return Promise.resolve(response.json()); // This will end up in SUCCESS part
    }
    return Promise.resolve(response.json()).then((responseInJson) => { // This will end up in ERROR part
        return Promise.reject(responseInJson.message); //  responseInJson.message = "Some nasty error message!"
    });
})
.then((result) => { // SUCCESS part
    console.log("Success: ", result); // Response from api in json
}, (error) => { // ERROR part
    // Because we rejected responseInJson.message, error will contain message from api. In this case "Some nasty error message!"
    console.log("Error: ", error); 
})
.catch(catchError => {
    console.log("Catch: ", catchError);
})
.finally(() => { ...});

Hope it helps :)

@garagepoort
Copy link

@Derimos My god, thank you! I had no idea why the body was not in the response.
Saved my day

@Blaiski
Copy link

Blaiski commented Sep 17, 2020

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)
  })
}

This was helpful, thanks for sharing

@AlessandroGambaro
Copy link

Hi,

i made this small package (code on github) following what I find here and in fetch documentation:

https://www.npmjs.com/package/fd-fetch-wrapper

Maybe this can help

@najibla
Copy link

najibla commented Nov 26, 2020

With Express, I am doing the following

export async function fetchData(token: any, body: any) {
  const result = await fetch(
    `${config.apiUrl}/${config.projectKey}/my-request`,
    {
      method: 'POST',
      headers: {
        Authorization: token,
      },
      body: JSON.stringify(body),
    }
  );

  const json = await result.json();
  if (!result.ok) {
    throw {
      statusCode: result.status,
      ...json
    };
  }
  return json;
}

and in the route I do

router.post(
  '/some-path',
  handleErrorAsync(async (req: Request, resp: Response, _err: Errback) => {
    const data = await fetchData(req.get('authorization'), req.body);
    resp.json(data)
  })
);

And I have an error handling package that does

// async functions errors handler to avoid using try catch for every async route

export const handleErrorAsync = (func: Function) => (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  func(req, res, next).catch((error: Error) => {
    next(error);
  });
};

// middleware to respond with an error when error caught
export function handleError(
  err: any,
  _req: Request,
  resp: Response,
  _next: NextFunction
) {
  if (err) {
    resp.status(err.statusCode || 500).json(err);
  }
}

And of course I add to the router

router.use(handleError);

@ghmcadams
Copy link

This could be simplified with the following:

return fetch(url)
    .then(res => {
        if (!res.ok) {
            return res.json().then(json => { throw json; });
        }
        return res.json();
    })
    .catch(err => {
        // err is not a promise
    });

@AnthonyZJiang
Copy link

I found this is easier to read:

return fetch(url)
    .then(async (res) => {
        if (!res .ok) {
            throw await res.json();
        }
        return response.json();
    })
    .catch(err => {
        // err is not a promise
    });

@szuecs-alex
Copy link

var handlejson = async function(x){
		var t = await x.text();
		try{
			return JSON.parse(t);
		}catch(e){				
			console.log("unparsed:",t);
			return null; //or throw new Expection(e);
		}
	}
fetch(...).then(handlejson)

@ariccio
Copy link

ariccio commented Jan 18, 2023

Oh I bet yall will love this extremely cursed wrapper I wrote for fetch a few years ago, it lets me bubble up the exact network error to the UI. With this monstrosity, I can get the actual error (e.g. ECONNREFUSED when the server isn't running!):

https://github.com/ariccio/COVID-CO2-tracker/blob/main/co2_client/src/utils/FetchHelpers.ts#L312

@commonpike
Copy link

Actually, I think the node-fetch docs propose a nice solution for the OP ?

Instead of throwing a regular Error, they throw a custom extension of Error that contains the response. In the catch clause, they access the response:

https://github.com/node-fetch/node-fetch?tab=readme-ov-file#handling-client-and-server-errors

class HTTPResponseError extends Error {
	constructor(response) {
		super(`HTTP Error Response: ${response.status} ${response.statusText}`);
		this.response = response;
	}
}
...
	if (!response.ok) {
		throw new HTTPResponseError(response);
	}
...
catch (error) {
	console.error(error);
	const errorBody = await error.response.text();
	....
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment