Exceptions in JavaScript Asynchronous Calls Are Not Caught

Table of Contents
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 originalopen
method is kept inoriginalOpen
, and the original method is executed viaoriginalOpen.call
.error
event listener
This event fires when anetwork 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, theerror
event will not occur. Therefore, it needs to be caught by theload
event. The HTTP status code is set inthis.status
. Errors like404 Not Found
or500 Internal Server Error
are handled here.patchXMLHttpRequest(window)
Overrides theXMLHttpRequest
prototype of the window object.patchXMLHttpRequest(iframeWindow)
If a page uses iframes, each iframe has its ownXMLHttpRequest
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 toerror
.
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 reject
ed 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
-
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.
-
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); } });
-
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();