Handling server errors with .catch

A few days ago, I built out the front end of a task management application. Everything was going smoothly, until I realized my front end had no ability to handle errors gracefully. For example

- What our user input was invalid?

-What if my server encountered an error?

- What if my front end attempted to access a resource that didn’t exist?

In these instances, I’d expect a response from the server, with some error messages. I’d want to break out of our normal execution path, so that app can doesn’t try to process this error data as if it were the JSON data it was expecting. I’d also want to log this error data, along with the HTTP response codes, if they weren’t included.

So I did a little research. Here’s what you need to know:

Promises have a .catch method

It takes a callback, and it works like this: You stick “.catch(callback)”onto the end of a promise. If the promise succeeds, then the ‘ then’ callback will be called, otherwise, the ‘catch’ callback will be called.

Fetch(‘notawebsite.com/api’).catch(error => console.log(’sorry, couldn’t connect’)
// "Sorry, couldn't connect

You can throw an error inside a then statement, and handle it inside the catch.

For example, a simple way of handling http errors is:

Fetch(‘website.com/api).then((resp) =>{
if(!resp.ok){ throw Error(resp.statusText)}
return resp.json()
}).then((json)=> doSomething(json))
. catch(console.log)

In the above example, if the server responds with a status code 300 or over, doSomething will never be called, instead, the catch callback will be called, and the responses statusText text will be logged.

You can also throw the response object itself, like so:

Fetch(‘website.com/api).then((resp) =>{
if(!resp.ok){throw resp}
return resp.json()
}).then((json)=> doSomething(json))
. catch( (resp)=>console.log(resp.status, resp.statusText))

Here, because we have the entire response object instead of just an error message, we are able log both the status and the statusText.

Or, you can use Promise.reject

Fetch(‘website.com/api).then((resp) =>{
if(!resp.ok){return Promise.reject(resp)}
return resp.json()
}).then((json)=> doSomething(json))
. catch( (resp)=>console.log(resp.status, resp.statusText))

Note the use of a return statement here. Errors break out of the function automatically, but rejecting the promise does not.

Don’t forget that order matters

Once you handle the error with .catch the promise chain will continue executing. So if you call .catch early, it will allow anything below it to execute, even if there was an error above it.

// lets say response is 404 Not Found
Fetch(‘website.com/api).then((resp) =>{
if(!resp.ok){return Promise.reject(resp)}
return resp.json()
})
.then((json)=> doSomething(json)) // doesn't run
. catch( (resp)=>console.log(resp.status, resp.statusText))
/ VS 
Fetch(‘website.com/api).then((resp) =>{
if(!resp.ok){return Promise.reject(resp)}
return resp.json()
})
. catch( (resp)=>console.log(resp.status, resp.statusText)) / runs
.then((json)=> doSomething(json)) // runs

Nested then statements allow you to retain scope

Say we are given an error in the JSON-API format. (See https://jsonapi.org/examples/#error-objects for more details.)

We could handle this error like so:

Fetch(‘website.com/api’).then((resp)=>resp.json())
.then((resp) => {
if (json.errors){showErrors(json.errors)}
else{doSomething(json)}})

In the code above, we are first checking if there are error messages, and if there are error messages, we pass them to another function to display the messages. But what if resp.errors doesn’t always include an error code (which we’ve decided to show the user?) Don’t fear:

Fetch(‘website.com/api’).then(function (resp) {
 return resp.json().then((json) => {
   if(!resp.ok){showErrors(resp.status,json.errors)}
   else{doStuff(json)}
  })
})

Here, we call resp.json inside the original then callback, with another callback chained on. Because we are still inside of the first ‘then’ callback, we still have access to the response object, along with access to our json data.

<Note>
In the code above, because we are inside a then, our callback function will wait for promise chain inside of it to resolve, and then return the result, rather than returning resp.json() itself.

.then( function(){ return resp.json().then(()=> 'bla')}) 
//is the same as
.then(()=> 'bla')

</note>

In the example above, we are breaking the promise chain because our we are ultimately returning nothing. Alternatively, we could combine the error and response object into a single object, and throw that object.

Fetch(‘website.com/api’).then(function (resp) {
 return resp.json().then((json) => {
   if(!resp.ok){
     let error = Object.assign({}, json, {
       respStatus: response.status,
       respStatusText: response.statusText
     })
     throw error
}
})
}).then((json)=> doSomething)
.catch((error)=>showErrors(error))

Handling Ruby on Rails Exceptions and Validation Errors

When rails encounters certain kinds of errors, including routing and active record errors, it’ll often return JSON that looks like this :

{
status: 404,
error: "Not Found",
exception: "#<ActiveRecord::RecordNotFound: Couldn't find User with 'id'=7>",
traces: {…}
}

We have a really helpful message here about what went wrong! It even includes HTTP code and status text. Let’s use what we’ve learned to check if the response is okay, and also log this JSON

Fetch(‘website.com/api’).then(function (resp) {
  return resp.json().then((json) => {
     if(!resp.ok){throw json}
})
.then((json)=> doSomething)// this json contains the normal response
.catch((json)=>console.log(json))//this json contains an error object

What about validation errors? This depends on what’s in the controller. One approach might be:

def create
@list=List.create(list_params)
if @list.valid?
render json: @list, status: :ok
else
render json: {errors: @list.errors}, status: 422
end

This isn’t JSON-API format, but it’s good enough for small projects. In the case of a http response with a status code over 300, lets have have our app log the whole JSON response object ( which could contain either validation errors or an exception message), and if our response has the errors key, assume those are validation errors and show them to the user .

Fetch(‘website.com/api’).then(function (resp) {
return resp.json().then((json) => {
if(!resp.ok){throw json}
})
.then((json)=> doSomething)// this json contains the normal response
.catch((json)=>{
console.log(json)
if(json.errors){showErrorsOnPage(json.errors)}
}

Errors handled!