Handling error cases in fetch javascript

This is a guide on using default fetch API. We will explore handling server errors, GET param and aborting requests in-flight.

Problem with default fetch

In js, the default way to make an API call is to use fetch and the default syntax goes something like this.

async function getTodos({title, id}) {
    let url = 'https://jsonplaceholder.typicode.com/todos'
    if(title){
        url += `?title=${title}`
    }
    if(id) {
        url += `&id=${id}`
    }
    try {
         const res = await fetch(url)
         const todos = await res.json()
         console.log(todos)
    } catch {
        console.log('some error')
    }
}
getTodos({title: 'lorem', id: 1}) // works

Here there are 2 potential problems

  • Error codes like 400, 404, 401 and 500 are unhandled.

  • Upon undefined or non-encoded get params. eg getTodos({title: 'lorem ipsum', id: 1}) or getTodos({id: 1})

Handling Error codes

Let’s remove the params because we are purely focusing on handing error

async function getTodos({userId, id}) {
    let url = 'https://jsonplaceholder.typicode.com/todos'
    try {
         const res = await fetch(url)
         if(!res.ok) {
            throw Error(response.statusText);
          }
         const todos = await res.json()
    } catch {
        console.log('some error')
    }
}
getTodos({userId: 1, id: 1})

Basically, server error in fetch wouldn’t reach the catch block. Instead, we need we manually check res.ok to see if we are good to go.

So then what would reach the catch block?

Consider the following code.

const controller = new AbortController();
const signal = controller.signal;
const cancelButton = $.querySelector('#cancel')
cancelButton.onClick = () => controller.abort()

try {
  const url = 'https://httpbin.org/delay/1';
  const response = await fetch(url, { signal });
} catch (error) {
  // DOMException: The user aborted a request.
  console.log('Error: ', error)
}

It would reach catch block in 2 scenarios when we use a controller.abort or due to network failure.

  • In our case when the user clicks on the cancel button. (DOM Error: Request abort by user)

  • When the user's device went offline while using the app. (DOM Error: Failed to fetch)

A better way to handle GET params

let url = new Url('https://jsonplaceholder.typicode.com/todos')
if(title){
    url.searchParams.set('title', title)
}
if(id) {
    url.searchParams.set('id', id)
}

This solves 2 problem

Final Version

const handleError = (res) => {
    if(res.ok) {
        return res
    }
    throw Error(res.statusText)
}

const constructSearch = (params) => {
    const search = new URLSearchParams()
    const dropNA = ([key, val]) => key != null && val != null
    const updateParam = ([key, val]) => {
        search.set(key, val)
    }
    Object.entries(params).filter(dropNA).forEach(updateParam)
    return search
}

const get = async (urlString, params, signal) => {
    let url = new URL(urlString, signal)
    url.search = constructSearch(params)
    try {
         const res = await fetch(url)
         handleError(res)
         const data = await res.json()
         return data;
    } catch(e) {
        console.log(e)
        throw e;
    }
}

const todoURL = 'https://jsonplaceholder.typicode.com/todos'
const getTodo = async (params) => get(todoURL, params)


getTodos({title: 'lorem', id: 1}) // works
getTodos({title: 'lorem ipsum', id: 1}) // works
getTodos({id: 1}) // works

When I knew about this nightmare, my brain was like.

Nightmares with js

So, what about Axios?