Fetch получить код ошибки

even though this is a bit old question I’m going to chime in.

In the comments above there was this answer:

const fetchJSON = (...args) => {
  return fetch(...args)
    .then(res => {
      if(res.ok) {
        return res.json()
      }
      return res.text().then(text => {throw new Error(text)})
    })
}

Sure, you can use it, but there is one important thing to bare in mind. If you return json from the rest api looking as {error: 'Something went wrong'}, the code return res.text().then(text => {throw new Error(text)}) displayed above will certainly work, but the res.text() actually returns the string. Yeah, you guessed it! Not only will the string contain the value but also the key merged together! This leaves you with nothing but to separate it somehow. Yuck!

Therefore, I propose a different solution.

fetch(`backend.com/login`, {
   method: 'POST',
   body: JSON.stringify({ email, password })
 })
 .then(response => {
   if (response.ok) return response.json();
   return response.json().then(response => {throw new Error(response.error)})
 })
 .then(response => { ...someAdditional code })
 .catch(error => reject(error.message))

So let’s break the code, the first then in particular.

.then(response => {
       if (response.ok) return response.json();
       return response.json().then(response => {throw new Error(response.error)})
})

If the response is okay (i.e. the server returns 2xx response), it returns another promise response.json() which is processed subsequently in the next then block.

Otherwise, I will AGAIN invoke response.json() method, but will also provide it with its own then block of code. There I will throw a new error. In this case, the response in the brackets throw new Error(response.error) is a standard javascript object and therefore I’ll take the error from it.

As you can see, there is also the catch block of code at the very end, where you process the newly thrown error. (error.message <— the error is an object consisting of many fields such as name or message. I am not using name in this particular instance. You are bound to have this knowledge anyway)

Tadaaa! Hope it helps!

I’ve been looking around this problem and has come across this post so thought that my answer would benefit someone in the future.

Have a lovely day!

Marek

May 23, 2022

Umar Hansa

On this page

  • Anticipate potential network errors
    • Examples of user errors
    • Examples of environmental changes
    • Examples of errors with the video-sharing website
  • Handle errors with the Fetch API
    • When the Fetch API throws errors
    • When the network status code represents an error
    • When there is an error parsing the network response
    • When the network request must be canceled before it completes
  • Conclusion

This article demonstrates some error handling approaches when working with the Fetch API. The Fetch API lets you make a request to a remote network resource. When you make a remote network call, your web page becomes subject to a variety of potential network errors.

The following sections describe potential errors and describe how to write code that provides a sensible level of functionality that is resilient to errors and unexpected network conditions. Resilient code keeps your users happy and maintains a standard level of service for your website.

Anticipate potential network errors #

This section describes a scenario in which the user creates a new video named "My Travels.mp4" and then attempts to upload the video to a video-sharing website.

When working with Fetch, it’s easy to consider the happy path where the user successfully uploads the video. However, there are other paths that are not as smooth, but for which web developers must plan. Such (unhappy) paths can happen due to user error, through unexpected environmental conditions, or because of a bug on the video-sharing website.

Examples of user errors #

  • The user uploads an image file (such as JPEG) instead of a video file.
  • The user begins uploading the wrong video file. Then, part way through the upload, the user specifies the correct video file for upload.
  • The user accidentally clicks «Cancel upload» while the video is uploading.

Examples of environmental changes #

  • The internet connection goes offline while the video is uploading.
  • The browser restarts while the video is uploading.
  • The servers for the video-sharing website restart while the video is uploading.

Examples of errors with the video-sharing website #

  • The video-sharing website cannot handle a filename with a space. Instead of "My Travels.mp4", it expects a name such as "My_Travels.mp4" or "MyTravels.mp4".
  • The video-sharing website cannot upload a video that exceeds the maximum acceptable file size.
  • The video-sharing website does not support the video codec in the uploaded video.

These examples can and do happen in the real world. You may have encountered such examples in the past! Let’s pick one example from each of the previous categories, and discuss the following points:

  • What is the default behavior if the video-sharing service cannot handle the given example?
  • What does the user expect to happen in the example?
  • How can we improve the process?
Action The user begins uploading the wrong video file. Then, part way through the upload, the user specifies the correct video file for upload.
What happens by default The original file continues to upload in the background while the new file uploads at the same time.
What the user expects The user expects the original upload to stop so that no extra internet bandwidth is wasted.
What can be improved JavaScript cancels the Fetch request for the original file before the new file begins to upload.
Action The user loses their internet connection part way through uploading the video.
What happens by default The upload progress bar appears to be stuck on 50%. Eventually, the Fetch API experiences a timeout and the uploaded data is discarded. When internet connectivity returns, the user has to reupload their file.
What the user expects The user expects to be notified when their file cannot be uploaded, and they expect their upload to automatically resume at 50% when they are back online.
What can be improved The upload page informs the user of internet connectivity issues, and reassures the user that the upload will resume when internet connectivity has resumed.
Action The video-sharing website cannot handle a filename with a space. Instead of «My Travels.mp4», it expects names such as «My_Travels.mp4» or «MyTravels.mp4».
What happens by default The user must wait for the upload to completely finish. Once the file is uploaded, and the progress bar reads «100%», the progress bar displays the message: «Please try again.»
What the user expects The user expects to be told of filename limitations before upload begins, or at least within the first second of uploading.
What can be improved Ideally, the video-sharing service supports filenames with spaces. Alternative options are to notify the user of filename limitations before uploading begins. Or, the video-sharing service should reject the upload with a detailed error message.

Handle errors with the Fetch API #

Note that the following code examples use top-level await (browser support) because this feature can simplify your code.

When the Fetch API throws errors #

This example uses a try/catch block statement to catch any errors thrown within the try block. For example, if the Fetch API cannot fetch the specified resource, then an error is thrown. Within a catch block like this, take care to provide a meaningful user experience. If a spinner, a common user interface that represents some sort of progress, is shown to the user, then you could take the following actions within a catch block:

  1. Remove the spinner from the page.
  2. Provide helpful messaging that explains what went wrong, and what options the user can take.
  3. Based on the available options, present a «Try again» button to the user.
  4. Behind the scenes, send the details of the error to your error-tracking service, or to the back-end. This action logs the error so it can be diagnosed at a later stage.
try {
const response = await fetch('https://website');
} catch (error) {
// TypeError: Failed to fetch
console.log('There was an error', error);
}

At a later stage, while you diagnose the error that you logged, you can write a test case to catch such an error before your users are aware something is wrong. Depending on the error, the test could be a unit, integration, or acceptance test.

When the network status code represents an error #

This code example makes a request to an HTTP testing service that always responds with the HTTP status code 429 Too Many Requests. Interestingly, the response does not reach the catch block. A 404 status, amongst certain other status codes, does not return a network error but instead resolves normally.

To check that the HTTP status code was successful, you can use any of the following options:

  • Use the Response.ok property to determine whether the status code was in the range from 200 to 299.
  • Use the Response.status property to determine whether the response was successful.
  • Use any other metadata, such as Response.headers, to assess whether the response was successful.
let response;

try {
response = await fetch('https://httpbin.org/status/429');
} catch (error) {
console.log('There was an error', error);
}

// Uses the 'optional chaining' operator
if (response?.ok) {
console.log('Use the response here!');
} else {
console.log(`HTTP Response Code: ${response?.status}`)
}

The best practice is to work with people in your organization and team to understand potential HTTP response status codes. Backend developers, developer operations, and service engineers can sometimes provide unique insight into possible edge cases that you might not anticipate.

When there is an error parsing the network response #

This code example demonstrates another type of error that can arise with parsing a response body. The Response interface offers convenient methods to parse different types of data, such as text or JSON. In the following code, a network request is made to an HTTP testing service that returns an HTML string as the response body. However, an attempt is made to parse the response body as JSON, throwing an error.

let json;

try {
const response = await fetch('https://httpbin.org/html');
json = await response.json();
} catch (error) {
if (error instanceof SyntaxError) {
// Unexpected token < in JSON
console.log('There was a SyntaxError', error);
} else {
console.log('There was an error', error);
}
}

if (json) {
console.log('Use the JSON here!', json);
}

You must prepare your code to take in a variety of response formats, and verify that an unexpected response doesn’t break the web page for the user.

Consider the following scenario: You have a remote resource that returns a valid JSON response, and it is parsed successfully with the Response.json() method. It may happen that the service goes down. Once down, a 500 Internal Server Error is returned. If appropriate error-handling techniques are not used during the parsing of JSON, this could break the page for the user because an unhandled error is thrown.

When the network request must be canceled before it completes #

This code example uses an AbortController to cancel an in-flight request. An in-flight request is a network request that has started but has not completed.

The scenarios where you may need to cancel an in-flight request can vary, but it ultimately depends on your use case and environment. The following code demonstrates how to pass an AbortSignal to the Fetch API. The AbortSignal is attached to an AbortController, and the AbortController includes an abort() method, which signifies to the browser that the network request should be canceled.

const controller = new AbortController();
const signal = controller.signal;

// Cancel the fetch request in 500ms
setTimeout(() => controller.abort(), 500);

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

Conclusion #

One important aspect of handling errors is to define the various parts that can go wrong. For each scenario, make sure you have an appropriate fallback in place for the user. With regards to a fetch request, ask yourself questions such as:

  • What happens if the target server goes down?
  • What happens if Fetch receives an unexpected response?
  • What happens if the user’s internet connection fails?

Depending on the complexity of your web page, you can also sketch out a flowchart which describes the functionality and user interface for different scenarios.

Return to all articles

For years, the default way to call APIs from a web page was XMLHttpRequest. But working with XMLHttpRequest was not easy, so on top of it were build lots of popular libraries like jQuery.ajax() or Axios who wrapped the XMLHttpRequest functionality in a Promise based interface and also hiding away its complexities.

These days browsers provide a better alternative to XMLHttpRequest: fetch(). Recently I migrated some code that used Axios to fetch. I was surprised by how hard that was.

Fetch has a few gotchas that we all should know about before we start using it, but using a native browser feature, should be preferred over a library, even for the simple fact that you can get rid of a few 3rd party kilobytes of JavaScript.

A simple example

// the only required parameter is the URL
fetch('http://api.open-notify.org/astros.json') 
    .then(response => {
        // do something with the response
    })
    .catch(error => {
        // do something with the error
    });

Fetch function returns a Promise.
A Promise can be:

  • resolved when it is successful (in this case when the response from the server is received )
  • rejected when it fails (in this case when the response from the server cannot be received)

Since the Promise is resolved/rejected at a later time, asynchronously, we need to register callback functions:

  • then is called when the promise is successful
  • catch is called when the promise fails

Or if you prefer async / await, you can use that too:

try {
    const response = await fetch(url) 
    // do something with the response
}
catch(err) {
    // do something with the error
}

But any useful fetch() is a bit more complicated.
The response of a fetch() request is a stream, so depending on the type of data returned by the server, you need to get the data from the stream.

For example, if the server response is JSON, you need to call response.json() that returns a Promise that resolves with the result of parsing the body text as JSON.

fetch('http://api.open-notify.org/astros.json') 
    .then(response => response.json() // or .text(), .blob(), .arrayBuffer(), .formData()
    .then(data => {
        // do something with data    
    })
    .catch(error => {
        // do something with the error
    });

We can use fetch() to load all kind of data, like image files, audio or video files:

fetch('/image.jpg')
    .then(response => response.blob()) // returns promise
    .then(blob => {
        image.src = URL.createObjectURL(blob);
    })
    .catch(error => {
        // do something with the error
    });

How to handle errors

Now here is something unexpected. Here is it, straight from MDN:

The Promise returned from fetch() won’t reject on HTTP error status even if the response is an HTTP 404 or 500. Instead, it will resolve normally (with ok status set to false), and it will only reject on network failure or if anything prevented the request from completing.

I have no idea why it’s working like this, none of the alternatives work this way, but this happens when you fetch an URL and the server responds with a 4xx or 5xx error:

fetch(url) 
    .then(response => {
        // This code is executed even for 4xx-5xx errors!
        // But the response will not contain expected data!
    })
    .catch(error => {
        // This is not called for 4xx-5xx errors!
    });

We need to detect these errors and handle them. The response has an ok flag that is set to false when the server responds with an error, so we can use that flag:

fetch(url) 
    .then(response => {
        if(!response.ok) { 
            const error = new Error(response.statusText || 'Fetch failed') 
            error.response = response;
            throw error; 
        } 
        return response; 
    }) 
    .then(response => response.json() // this is skipped for 4xx-5xx errors!
    .catch(error => {
        // this is now called for 4xx-5xx errors!
    });

We can extract error related code in a separate function so we can use it with multiple fetch calls:

function checkForErrors(response) {
    if(!response.ok) { // 
        const error = new Error(response.statusText || 'Fetch failed') 
        error.response = response;
        throw error; // or Promise.reject(error)
    } 
    return response;
}

fetch(url) 
    .then(checkForErrors) 
    .then(response => response.json() // this is skipped for 4xx-5xx errors!
    .catch(error => {
        // this is now called for 4xx-5xx errors!
    });

Advanced usage

Besides the URL, fetch accepts an object with different options

fetch(url, options) 
    .then(response => {})
    .catch(error => {});

Here they are (those with * in front are the default values)

{
    method: 'POST', // *GET, POST, PUT, DELETE, etc.
    mode: 'cors', // no-cors, *cors, same-origin
    cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
    credentials: 'same-origin', // include, *same-origin, omit
    headers: {
      'Content-Type': 'application/json' // 'application/x-www-form-urlencoded', multipart/form-data, text/plain
    },
    redirect: 'follow', // manual, *follow, error
    referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
    body: JSON.stringify(data) // body data type must match "Content-Type" header
  }

This means the following are the same:

fetch(url) 
    .then(response => {})
    .catch(error => {});

fetch(url, {
        method: 'GET',
        mode: 'cors', 
        cache: 'default', 
        credentials: 'same-origin', 
        headers: {
            'Content-Type': 'application/json'
        },
        redirect: 'follow', 
        referrerPolicy: 'no-referrer-when-downgrade', 
        body: ''
    }) 
    .then(response => {})
    .catch(error => {});

Let’s dive into some of the options and see how we can use them.

method option

By default fetch() will make a GET request.
If you want to do a POST you’ll need to set the method option to POST. Usually, you’ll also send some data:

const data = { user: 'Jon', city: 'London'}

fetch(url, 
    {  
        method : 'POST'  
        headers: { 
            'Content-Type': 'application/json', 
        },
        body: JSON.stringify(data), 
    }) 
    .then(response => {})
    .catch(error => {});

mode option

The fetch mode can be cors, no-cors, or same-time.

fetch(url, 
    {  
       mode: 'cors'
    }) 
    .then(response => {})
    .catch(error => {});

CORS headers are used by some servers to accept requests only from certain domains (e.g. company.com doesn’t accept requests from others.com)
By default, the cors mode is used by fetch. This means that if the server doesn’t have the CORS headers set correctly, the fetch will be canceled. These are the most frustrating errors I ever encountered.

credentials option

To access some resources on some servers you need to authorized, e.g. to read your Gmail you need to be logged in with your Google credentials.

You are asked to log in once, and any subsequent fetch requests made by your browsers are allowed if your requests include the credentials.

fetch(url, 
    {  
       credentials: 'include'
    }) 
    .then(response => {})
    .catch(error => {});

Usually, credentials are saved as cookies, and thus when you use credentials include, all cookies are sent to the server. And this can sometimes create problems, as some servers accept
only a limited length of cookies.

Timeout

Sadly fetch() doesn’t provide out-of-the-box support for timeouts. This means that the fetch request will wait forever for the server to respond.

Luckily we can implement this by wrapping the fetch in a Promise that we can resolve or reject:

// create a wrapper
function fetchTimeout(url, options, timeout = 5000)
    // return the promise
    return new Promise(resolve, reject) {

        const timer = setTimeout(() => {
            reject(new Error('Request timed out'));
        }, timeout);

        const clearTimer = response => {
            clearTimout(timer);
            return response;
        }

        fetch(url, options)
            .then(clearTimer) // clear the timer
            .then(resolve)
            .catch(reject);
    }
}

// use the wrapper instead of fetch
fetchTimeout(url, {}, 10000) 
    .then(response => response.json() 
    .then(data => {
        // do something with data    
    })
    .catch(error => {
        // do something with the error
    });

Cancel

Sometimes we want to cancel a fetch().

Let’s assume you are on Youtube or Netflix, you fetch a video but then you change your mind and want to see another video. You start fetching the new one, but what happens with the other one? You don’t want to see it anymore so you want to cancel it. How you do that?

Well, you can use AbortController, a shiny new experimental technology! (FYI It’s already available in all major browsers)

// create a controller
const controller = new AbortController();
const { signal } = controller;

// call abort() if you want to cancel it
controller.abort();

Here is how you can use it with fetch, you pass the signal as an option:

const controller = new AbortController();
const { signal } = controller;

fetch(url, 
    { 
        signal  
    })
    .then(response => {})
    .catch(error => {
        if (error.name === 'AbortError') { 
            console.log('Fetch aborted');
        } else {
            // error not caused by abort
        }
    });


// Abort request
controller.abort();

If you pass the same signal to multiple fetch calls, controller.abort() will cancel all requests with that signal.

If you call .abort() after the fetch has completed, nothing happens, abort is ignored.

It took a long time for the Abort API to be accepted:

The key disagreement is one group wanted the abort method to exist on the object returned by fetch(), whereas the other wanted a separation between getting the response and affecting the response. — Jake Archibald

I would prefer the object returned by fetch() to have the abort method. Since is best to keep the abort details hidden we would need to create a wrapper like this:

function abortableFetch(request, opts) {
  const controller = new AbortController();
  const signal = controller.signal;

  return {
    abort: () => controller.abort(),
    ready: fetch(request, { ...opts, signal })
  };
}

Because the above solution breaks the interface of the object returned by fetch(), we could add the abort() on that object (e.g. as proposed by the group that lost)

function abortableFetch(url, options) {
    const controller = new AbortController();
    const signal = controller.signal;

    // return the promise
    const promise = new Promise(resolve, reject) {
        fetch(url, {...options, signal)
            .then(resolve)
            .catch(reject);
    }
    promise.abort = () => controller.abort();

    return promise;
}

This allows us to combine the timeout and cancel functionality in a single wrapper:


/**
 * Fetch that can timeout and is cancellable
 */
function enhancedFetch(url, options, timeout = 5000) {
    const controller = new AbortController();
    const signal = controller.signal;

    const timer = setTimeout(() => {
        reject(new Error('Request timed out'));
    }, timeout);

    const clearTimer = response => {
        clearTimout(timer);
        return response;
    }

    // return the promise
    const promise = new Promise(resolve, reject) {
        fetch(url, {...options, signal)
            .then(clearTimer) // clear the timer
            .then(resolve)
            .catch(reject);
    }
    promise.abort = () => controller.abort();

    return promise;
}

Progress

We can track the download progress (but not upload progress) using response.body, that is a ReadableStream, a source of data, from which we can read data as it becomes available.

Unlike response.json() and other methods, response.body gives full control over the reading process, and we can see how much data is received at any moment.

const progressIndicator = (length, total) => {...}

fetch('https://reqres.in/api/users/1') 
    .then(response => {
        // get reader from response body
        const reader = response.body.getReader();
        // get total length
        const contentLength = +response.headers.get('Content-Length');
        let receivedLength = 0; 
        let chunks = []; 

        while(true) {
            const { done, value } = await reader.read();

            if (done) {
                break;
            }

            chunks.push(value);
            receivedLength += value.length;
            console.log(`Received ${receivedLength} of ${contentLength}`);
            // here you can call a function with the current length
            progressIndicator(receivedLength, contentLength)
        }

        // when all data is available it's time to parse it
        let contentArray = new Uint8Array(receivedLength); 
        let position = 0;
        for(let chunk of chunks) {
            contentArray.set(chunk, position); 
            position += chunk.length;
        }
        // decode content array into a string
        const result = new TextDecoder("utf-8").decode(contentArray);
        // finally get data
        const data = JSON.parse(result);
    })
    .catch(error => {});

Polyfill

All major browsers support fetch these days, but if you want support for IE11 or some other old browser, you’ll need to use a polyfill (like https://github.com/github/fetch)

Resources

  • https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
  • https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
  • https://developer.mozilla.org/en-US/docs/Web/API/AbortController
  • https://github.github.io/fetch/#response-body
  • https://davidwalsh.name/fetch-timeout
  • https://davidwalsh.name/fetch
  • https://fetch.spec.whatwg.org/
  • https://developers.google.com/web/updates/2017/09/abortable-fetch
  • https://javascript.info/fetch-progress

Thanks for reading. This article was originally posted on my blog.
Cover Photo by K. Mitch Hodge on Unsplash

  • Fetch is not defined ошибка
  • Fetch first git ошибка
  • Fetch event respondwith received an error returned response is null ошибка
  • Fetch async await обработка ошибок
  • Fetch assoc php ошибка