# Why even simple window.fetch might be tricky

I recently saw this tweet [https://twitter.com/thomasfindlay94/status/1672211922517622784](https://twitter.com/thomasfindlay94/status/1672211922517622784):

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1687551348958/b34d6f24-4276-4ef9-aff8-290642aca61b.png align="center")

While many responses were related to missed `await` before fetch, I'd like to talk about a bigger issue: error handling.

Let's fix an obvious error and take a closer look at this code block:

```javascript
const fetchData = async () => {
    const response = await fetch('https://website.com');
    const data = await response.json();
    return data;
}
```

`fetchData` returns a `Promise` with a parsed JSON value (object) inside.

How many error-handling blocks do we need here? Would it be sufficient to have a single try-catch?

```javascript
const fetchData = async () => {
    try {
        const response = await fetch('https://website.com');
        const data = await response.json();
        return data;
    } catch (e) {
       // Error reporting goes here
    }
}
```

The short answer here is no, it's not the best possible design.

We have only 1 catch entry point, which should handle all the work:

`e` can be:

1. Network issue:
    
    1. Unstable connection, e.g. timeout
        
    2. User goes offline
        
    3. Other problems like CORS:
        
        ![Example of CORS error](https://cdn.hashnode.com/res/hashnode/image/upload/v1688216718622/c420938d-f2f4-4c06-a390-3ac009e48e67.png align="center")
        
    4. Parsing errors, when the received content is not valid JSON or not a JSON at all
        

That's the reason why sometimes engineers wrap every `await` in `try-catch`:

```javascript
const fetchData = async () => {
    let response; // We have to move response on top
    try {
        response = await fetch('https://website.com');
    } catch (e) {
        // Handle network errors
        return errorState;
    }
    try {
        const data = await response.json();
        return data;
    } catch (e) {
       // Handle parsing/serialisation error
    }
}
```

Additionally, when we return `data` we cannot be sure of what would be the output. The request might get an error code response, as the server returns something different from 2xx:

1. Sometimes it is worth asking the user to re-login (e.g. 401)
    
2. Sometimes we should tell users they have no permission (403)
    
3. In some cases, validation might fail (400)
    
4. We might want to schedule a retry or ask user to try again later (5xx)
    
5. and etc.
    

And yes, the error code response would fulfill the original `await` promise:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1688216584388/64d3d7c2-1afc-4f73-bda7-573186415bc7.png align="center")

It would complicate our code furthermore:

```javascript
const fetchData = async () => {
    let response; // We have to move response on top
    try {
        response = await fetch('https://website.com');
    } catch (e) {
        // Handle network errors
        return someErrorState;
    }
    // Request is successfull if any of the following conditions are true:
    // response.ok === true
    // response.status === 200
    // response.status >= 200 && response.status < 300
    if (!response.ok) {
        switch (response.status) {
            case 401: {
                // Need re-login
                return someErrorState;
            }
            ...
            default: 
                // Other errors
                return someErrorState;
        }
    }

    try {
        const data = await response.json();
        return data;
    } catch (e) {
       // Handle parsing/serialisation error
    }
}
```

This code looks way more like spaghetti rather than something you can read, right?

But let's spend a little bit more time handling the last case: *When a server returns an error code, it also can add a payload to identify an error exactly!*

The most common usage for that is 400 error code during form validations or so. That means we should do `response.json()` even when `response.ok === false`. While doing so we should remember that `response.json()` might reject the promise:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1688217418843/cd273d49-8019-4608-9128-e5109fae298b.png align="center")

The most "correct" way is to verify `content-type` header of the `Response` before calling `.json` or so. It's stored in `Response.headers` field:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1688217621300/c2659337-f4b5-4d95-8950-24b09480c030.png align="center")

However:

1. It would over-complicate the code for the cases where we know for sure we expect `.json` format;
    
2. There still can be a "corrupted" json.
    

Let's make it all together and start brushing up:

```javascript
function parseResponseAsJson(response) {
    try {
        const data = await response.json();
        return data;
    } catch (e) {
       // Handle parsing/serialisation error
    }
}

const fetchData = async () => {
    let response; // We have to move response on top
    try {
        response = await fetch('https://website.com');
    } catch (e) {
        // Handle network errors
        return someErrorState;
    }
    // Request is successfull if any of the following conditions are true:
    // response.ok === true
    // response.status === 200
    // response.status >= 200 && response.status < 300
    if (!response.ok) {
        switch (response.status) {
            case 400: {
                // Error, but we expect JSON with details
                return parseResponseAsJson(response);
            }
            case 401: {
                // Need re-login
                return someErrorState;
            }
            ...
            default: 
                // Other errors
                return someErrorState;
        }
    }
    return parseResponseAsJson(response);   
}
```

Overall we have a huge bunch of code just to make a simple request. That's insane!

# How to make it simpler?

Let's start reducing the complexity of the code.

To do so, we should define the contract first.

We expect `fetchData` to return some `TResponse` from the server as a success. Additionally, we have a number of error states, which we need to support. Let's categorize them:

1. NetworkError
    
2. ParseError
    
3. 400 error code
    
4. Login Error
    
5. ServerError
    
6. (Something else depending on a task)
    

For simplicity, we can say we expect only `application/json` content type.

Here are the steps which we can make:

1. Create a clear contract for developers
    
2. Combine back 2 `await` inside single `try-catch` block. To make it coorect we should distinguish, which line raised an `Error`. To do so, we should keep `response` variable out of `try-catch` block
    
3. Keep "a good path" as short as possible and let all the error handling be placed either before or after "a good result"
    
4. Give the power to handle error codes to the developer, who would call our method. They should be able to have a `switch` statement on their side, but `json` results should be available to them.
    
5. \[Optional\] Add simple typing
    

Let's implement it:

```typescript
type ResponseResult<TResponse> =
  | {
      ok: true;
      data: TResponse;
    }
  | {
      ok: false;
      errorCode: number;
      // If you expect in your app particular type of errors,
      // you can define errorData, or event specific pairs of
      // errorCode+errorData
      errorData: void | unknown;
    }
  | {
      ok: false;
      error: any;
    };

export const fetchData = async <TResponse>(
  url: string
): Promise<ResponseResult<TResponse>> => {
  let response: void | Response; // We have to move response on top
  try {
    response = await fetch(url);
    const parsed = await response.json();
    if (response.ok) {
      return {
        ok: true,
        data: parsed as TResponse
      };
    }

    return {
      ok: false,
      errorCode: response.status,
      errorData: parsed
    };
  } catch (e) {
    // we should remember that response might fail due to Network issue
    if (typeof response === "object") {
      if (response.ok) {
        return {
          ok: false,
          error: e
        };
      }
      return {
        ok: false,
        errorCode: response.status,
        errorData: null
      };
    }
    return {
      ok: false,
      error: e
    };
  }
};
```

Sandbox: [https://codesandbox.io/s/dank-hooks-6pxyqp](https://codesandbox.io/s/dank-hooks-6pxyqp)

So... after several changes we finally did the version of `fetch` which looks way better than at the very beginning, right?

We have error handling, parsing, response processing, and status code support, which should be sufficient for many common `fetch` cases.

## The journey isn't ended tho

We still owe:

1. `fetch` configuration support. It is simple to do, we just need to extend function arguments
    
2. `Abort` / `AbortController` support
    
3. Add support for data types different from `json` for `ReadableStream`.
    
4. XSRF support
    
5. Better typing: e.g. better types for expected errors such as 4xx or so
    
6. etc.
    

And after a few more iterations we can get a utility library to handle requests for your project in the right way!

The alternative solution is to use a ready library: [axios](https://github.com/axios/axios), [superagent](https://github.com/ladjs/superagent), [ofetch](https://github.com/unjs/ofetch) or similar
