DEV Community

loading...
Cover image for Handling requests with fetch

Handling requests with fetch

sarahob profile image Sarah 🦄 ・4 min read

This week I got to rewrite our apps requests from using axios to using the Fetch API.

Let's get into it!

Wasn't that supposed to fail?

try {
    const response = await fetch(`${url}`, requestOptions);
    return await handleResponse(response);
  } catch (error) {
    return Promise.reject(error);
  }
Enter fullscreen mode Exit fullscreen mode

Looking at the code above, you might expect that if the request responds with an error such as 404 or 500 it would be caught and rejected. Nice and tidy, right?

Nope. The catch will only get invoked if the request does not complete, for instance in a network failure. If the request returns an error it will resolve normally but ok will be set to false. (ok is a property on the HTTP response.)

How do I handle my errors then?

So your api might typically implement the following pattern; try to make a request, do something if it's successful, if it fails; catch the error and log it.

    try {
         const result = fetch(url);
            // do something after request succeeds
    } catch (e) {
         // log error
                // notify user something went wrong
    }
Enter fullscreen mode Exit fullscreen mode

So with this in mind, we can look at our fetch code in the first code block and see that if the request returns an error, it will not trigger the catch because it's not rejected, it still resolves as normal. We don't want this, we want the catch to be triggered so our error gets logged and our user gets notified that something went wrong.

Handle it

async function handleResponse(response) {
  if (response.status === 204) {
    return Promise.resolve({});
  } else if (response.status >= 200 && response.status < 300) {
    const json = await response.json();
    return Promise.resolve(json);
  } else {
    const error = await response.json();
    return Promise.reject(error);
  }
}
Enter fullscreen mode Exit fullscreen mode

To handle this I wrote a handleResponse function. This function takes the response returned from the fetch and checks the status. (Here I wanted to specifically check the status's to handle different cases but you could also check the ok property.)

In the code above you can see that a status of 204 will resolve with an empty object, that's because 204 is a No-Content response and so there is nothing to unwrap.

For any response between 200 and 300 we unwrap the json and resolve the promise with the data.

Otherwise we resolve the json and reject the promise with the error. This reject will invoke the catch in our saga thus logging the error and notifying the user.

Wrap it up

I decided to write a wrapper function which would encapsulate most of this fetch logic. This way fellow developers can easily make requests without worrying about unwrapping, and resolving or rejecting responses for each request.

Another benefit is that the Authorization headers get set in one place and are always attached to each request.

Below is the fetchRequestWrapper in Typescript. We still give a lot of control to the user but ensure that responses and errors are handled in a consistent way.

async function handleResponse(response: any) {
  if (response.status === 204) {
    return Promise.resolve({});
  } else if (response.status >= 200 && response.status < 300) {
    const json = await response.json();
    return Promise.resolve(json);
  } else {
    const error = await response.json();
    return Promise.reject(error);
  }
}

interface IFetchRequestOptions {
  method: HTTPMethods;
  data?: any;
  headers?: { [key: string]: string };
}

export const fetchRequest = async (url: string, options?: IFetchRequestOptions) => {
  const authHeader = `Get your auth token`;

  const requestOptions: any = {
    method: options?.method ? options.method : HTTPMethods.GET,
    body: JSON.stringify(options?.data),
    headers: {
      Authorization: authHeader,
      ...options?.headers,
    },
  };

  try {
    const response = await fetch(`${url}`, requestOptions);
    return await handleResponse(response);
  } catch (error) {
    return Promise.reject(error);
  }
};

Enter fullscreen mode Exit fullscreen mode

Additional GOTCHAs:

These are some little things I ran into that caught me for a bit.

Posting JSON:

When using POST with fetch to send JSON to the server there are two main things to remember.

First, the Content-Type header needs to be set as application/json.

headers: { 'Content-Type': 'application/json' }
Enter fullscreen mode Exit fullscreen mode

Second, the data you pass in the body needs to be wrapped in JSON.stringify

body: JSON.stringify(data)
Enter fullscreen mode Exit fullscreen mode

Uploading data:

Some of our requests require users to upload a file. This had me stumped for a few hours because the request kept failing even though I was setting the Content-Type header to multi-part/form-data which I thought was required.

I luckily stumbled across this post which helped resolve the issue. The main learning was that when uploading data don't set the Content-Type header, if you don't the browser will do it automatically and add the web-boundary required for uploads.

Additional tip: if you are using TypeScript make sure to the body is of type FormData.

In the wrapper I decided it would be cleaner to add a separate function to handle uploads in order to separate the different functionality and not clutter up the main fetch request. Here is the fetch upload function in Typescript. You can see the interface for request options is much stricter here and the method is always POST.

interface IFetchRequestUploadOptions {
  data: FormData;
}

export const fetchRequestUpload = async (url: string, options: IFetchRequestUploadOptions) => {
  const authHeader = `Get your auth token`;

  const requestOptions: any = {
    method: HTTPMethods.POST,
    body: options.data,
    headers: {
      Authorization: authHeader,
    },
  };

  try {
    const response = await fetch(`$url}`, requestOptions);
    return await getResponse(response);
  } catch (error) {
    return Promise.reject(error);
  }
};
Enter fullscreen mode Exit fullscreen mode

And that's it, that was my journey with fetch. Thanks for reading! If you liked it please like and share! I hope this helps you with your coding journey!

meangirls-reference-that's-so-fetch

Discussion (0)

Forem Open with the Forem app