Exceptions in JavaScript Asynchronous Calls Are Not Caught

Programming
Published on August 13, 2025
Article Image

Introduction

Exceptions occurring in XMLHttpRequest or fetch cannot be caught by the following error event listener:

window.addEventListener("error", function (event) {
    // Some processing
})

MDN 1 explains it as follows:

This means that uncaught exceptions occurring in asynchronous communication processes like XMLHttpRequest or fetch are not caught by the global error event listener. Since fetch is Promise-based, unhandled rejections can be caught globally with unhandledrejection.

Type of Exception Catchable Events
JavaScript execution error window.onerror / window.addEventListener("error", …)
Image/script loading error Same as above
fetch failure try...catch / .catch() / window.addEventListener("unhandledrejection", …)
XMLHttpRequest failure xhr.addEventListener("error", …) / "timeout", "abort"

The problem lies with XMLHttpRequest, where handling XMLHttpRequest errors requires setting event listeners for each XMLHttpRequest instance. Therefore, if you want to centralize error handling, you can deal with it using the following method.

Centralizing XMLHttpRequest Error Handling

// Patches XMLHttpRequest of the specified window object and performs common error handling
function patchXMLHttpRequest(targetWindow) {
    const originalOpen = targetWindow.XMLHttpRequest.prototype.open;

    // Overwrite XMLHttpRequest's open method using prototype extension
    targetWindow.XMLHttpRequest.prototype.open = function (method, url, async) {
        // error event listener 
        this.addEventListener("error", function () {
            // Processing for network errors
        });

        // load event listener 
        this.addEventListener("load", function () {
            if (this.status >= 200 && this.status < 300) {
                // If the HTTP status code is in the 200 range, it is considered successful.               
            } else if (this.status >= 400) {
                // If the HTTP status code is 400 or higher, it is considered an error.
                // Processing for errors
            }
        });

        // Execute the original open method
        return originalOpen.call(this, method, url, async);
    };
}

// Apply patchXMLHttpRequest to the window object
patchXMLHttpRequest(window);

// Apply patchXMLHttpRequest to all iframes
const iframes = document.querySelectorAll("iframe");
iframes.forEach((iframe) => {
    iframe.addEventListener("load", () => {
        const iframeWindow = iframe.contentWindow;
        patchXMLHttpRequest(iframeWindow);
    });
});
  • Overwriting XMLHttpRequest's open method using prototype extension
    The original open method is kept in originalOpen, and the original method is executed via originalOpen.call.

  • error event listener
    This event fires when a network error occurs. Network errors include cases where TCP communication with the web server fails, such as:

Error Message Content Possible Causes
ERR_NAME_NOT_RESOLVED Name resolution failed Incorrect hostname or DNS configuration
ERR_ADDRESS_UNREACHABLE IP address unreachable DNS name resolution succeeded, but unable to connect to the IP address (e.g., routing error)
ERR_CONNECTION_REFUSED Server refused connection Connectivity to the server was established, but the port is closed, or the web server is not running
ERR_CONNECTION_TIMED_OUT Connection request sent to server, but no response, timed out Connectivity to the server was established, but the web server was not running or too busy to respond

Note that the XMLHttpRequest error event listener cannot obtain specific error messages or status codes. (While network error messages can be checked in browser developer tools, JavaScript does not receive information about the cause of errors due to security reasons.)

  • load event listener
    Even if TCP communication itself completes successfully, if the HTTP status code indicates an error, the error event will not occur. Therefore, it needs to be caught by the load event. The HTTP status code is set in this.status. Errors like 404 Not Found or 500 Internal Server Error are handled here.

  • patchXMLHttpRequest(window)
    Overrides the XMLHttpRequest prototype of the window object.

  • patchXMLHttpRequest(iframeWindow)
    If a page uses iframes, each iframe has its own XMLHttpRequest prototype, so all of them need to be overwritten. (Only iframes from the same origin can be overwritten.)

  • Regarding timeout, abort
    Although not included in the sample code, they can be handled similarly to error.

In the case of fetch

In the case of fetch, errors can be caught globally with the unhandledrejection event listener.

window.addEventListener("unhandledrejection", (event) => {
    // Processing for errors
});

However, fetch also has a pitfall: as the event name unhandledrejection implies, only cases where fetch is rejected can be caught here. Specifically, this applies to failures in TCP communication. HTTP errors such as 404 Not Found or 500 Internal Server Error cannot be handled here. Therefore, just like the distinction between XMLHttpRequest's error and load, fetch also requires a distinction.
To determine HTTP errors with fetch, use the ok property of the Response object.

fetch("https://hoge.com/fuga")
    .then(res => {
        if (!res.ok) {
            // Processing for HTTP errors
            // res.status contains the HTTP status code.
        }
    });

As a way to catch HTTP errors globally, you can override fetch.

// Save the original fetch
const originalFetch = window.fetch;

window.fetch = async (...args) => {
  const res = await originalFetch(...args);

  // Treat HTTP errors as rejections
  if (!res.ok) {
    return Promise.reject(new Error(`HTTP error ${res.status}`));
  }

  return res;
};

By doing this, unhandledrejection can be fired even in the case of HTTP errors.

One caveat is that this method changes the behavior of fetch, which could affect existing code that uses fetch. For example, if there was code that assumed 404 Not Found would be treated as a success and processing would continue, applying this change would cause it to reject, altering its behavior.

Summary

The methods for determining each type of error for XMLHttpRequest and fetch are summarized in the table below.

Type of Error XMLHttpRequest fetch
TCP communication error error try...catch / .catch() / unhandledrejection
HTTP error load Response object's ok property
Timeout timeout setTimeout + AbortController.abort() 2
Abort abort AbortController.abort() 3

Note: fetch does not have timeout properties or abort methods like XMLHttpRequest. Instead, AbortController is used.

This document introduced methods for implementing global error handling for XMLHttpRequest and fetch. The use case envisioned is when there is a need to add common error handling or error logging post-factum. Ideally, the implementation would involve creating a common component that wraps XMLHttpRequest and fetch beforehand, and performing asynchronous communication through this common component. We hope this information is helpful.

Footnotes

  1. Window: error event

    This event is only generated for script errors thrown synchronously, such as during initial loading or within event handlers. If a promise was rejected (including an uncaught throw within an async function) and no rejection handlers were attached, an unhandledrejection event is fired instead.

  2. Timeout example

    function fetchWithTimeout(url, ms) {
    const controller = new AbortController();
    const id = setTimeout(() => controller.abort(), ms);
    
    return fetch(url, { signal: controller.signal })
        .finally(() => clearTimeout(id));
    }
    
    // fetch that times out in 5 seconds
    fetchWithTimeout("https://example.com/data", 5000)
    .then(response => console.log("Success:", response))
    .catch(err => {
        if (err.name === "AbortError") {
            console.error("Aborted due to timeout");
        } else {
            console.error("Other error:", err);
        }
    });
    

  3. Abort example

    const controller = new AbortController();
    
    fetch("https://example.com/data", { signal: controller.signal })
    .then(response => console.log("Success:", response))
    .catch(err => {
        if (err.name === "AbortError") {
            console.error("User aborted");
        }
    });
    
    // Cancel due to user action, etc.
    controller.abort();