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 originalopenmethod is kept inoriginalOpen, and the original method is executed viaoriginalOpen.call.errorevent listener
This event fires when anetwork erroroccurs. 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.)
loadevent listener
Even if TCP communication itself completes successfully, if the HTTP status code indicates an error, theerrorevent will not occur. Therefore, it needs to be caught by theloadevent. The HTTP status code is set inthis.status. Errors like404 Not Foundor500 Internal Server Errorare handled here.patchXMLHttpRequest(window)
Overrides theXMLHttpRequestprototype of the window object.patchXMLHttpRequest(iframeWindow)
If a page uses iframes, each iframe has its ownXMLHttpRequestprototype, 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 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
-
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();