Skip to content

Commit d1dd430

Browse files
authored
feat: overlay displays unhandled promise rejection (#4849)
1 parent 51f8a1b commit d1dd430

File tree

10 files changed

+395
-37
lines changed

10 files changed

+395
-37
lines changed

client-src/overlay.js

+24-9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ansiHTML from "ansi-html-community";
55
import { encode } from "html-entities";
66
import {
77
listenToRuntimeError,
8+
listenToUnhandledRejection,
89
parseErrorToStacks,
910
} from "./overlay/runtime-error.js";
1011
import createOverlayMachine from "./overlay/state-machine.js";
@@ -282,16 +283,13 @@ const createOverlay = (options) => {
282283
});
283284

284285
if (options.catchRuntimeError) {
285-
listenToRuntimeError((errorEvent) => {
286-
// error property may be empty in older browser like IE
287-
const { error, message } = errorEvent;
288-
289-
if (!error && !message) {
290-
return;
291-
}
292-
286+
/**
287+
* @param {Error | undefined} error
288+
* @param {string} fallbackMessage
289+
*/
290+
const handleError = (error, fallbackMessage) => {
293291
const errorObject =
294-
error instanceof Error ? error : new Error(error || message);
292+
error instanceof Error ? error : new Error(error || fallbackMessage);
295293

296294
const shouldDisplay =
297295
typeof options.catchRuntimeError === "function"
@@ -309,6 +307,23 @@ const createOverlay = (options) => {
309307
],
310308
});
311309
}
310+
};
311+
312+
listenToRuntimeError((errorEvent) => {
313+
// error property may be empty in older browser like IE
314+
const { error, message } = errorEvent;
315+
316+
if (!error && !message) {
317+
return;
318+
}
319+
320+
handleError(error, message);
321+
});
322+
323+
listenToUnhandledRejection((promiseRejectionEvent) => {
324+
const { reason } = promiseRejectionEvent;
325+
326+
handleError(reason, "Unknown promise rejection reason");
312327
});
313328
}
314329

client-src/overlay/runtime-error.js

+18-1
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,21 @@ function listenToRuntimeError(callback) {
3030
};
3131
}
3232

33-
export { listenToRuntimeError, parseErrorToStacks };
33+
/**
34+
* @callback UnhandledRejectionCallback
35+
* @param {PromiseRejectionEvent} rejectionEvent
36+
* @returns {void}
37+
*/
38+
39+
/**
40+
* @param {UnhandledRejectionCallback} callback
41+
*/
42+
function listenToUnhandledRejection(callback) {
43+
window.addEventListener("unhandledrejection", callback);
44+
45+
return function cleanup() {
46+
window.removeEventListener("unhandledrejection", callback);
47+
};
48+
}
49+
50+
export { listenToRuntimeError, listenToUnhandledRejection, parseErrorToStacks };

examples/client/overlay/README.md

+51
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,54 @@ npx webpack serve --open --no-client-overlay
3131
2. You should see an overlay in browser for compilation errors.
3232
3. Update `entry` in webpack.config.js to `app.js` and save.
3333
4. You should see the text on the page itself change to read `Success!`.
34+
35+
## Additional Configurations
36+
37+
### Filter errors by function
38+
39+
**webpack.config.js**
40+
41+
```js
42+
module.exports = {
43+
devServer: {
44+
client: {
45+
overlay: {
46+
runtimeErrors: (msg) => {
47+
if (msg) {
48+
if (msg instanceof DOMException && msg.name === "AbortError") {
49+
return false;
50+
}
51+
52+
let msgString;
53+
54+
if (msg instanceof Error) {
55+
msgString = msg.message;
56+
} else if (typeof msg === "string") {
57+
msgString = msg;
58+
}
59+
60+
if (msgString) {
61+
return !/something/i.test(msgString);
62+
}
63+
}
64+
65+
return true;
66+
},
67+
},
68+
},
69+
},
70+
};
71+
```
72+
73+
Run the command:
74+
75+
```shell
76+
npx webpack serve --open
77+
```
78+
79+
What should happens:
80+
81+
1. When you click the "Click to throw error" button, the overlay should appears.
82+
1. When you click the "Click to throw ignored error" button, the overlay should not appear but you should see an error is logged in console (default browser behavior).
83+
1. When you click the "Click to throw unhandled promise rejection" button, the overlay should appears.
84+
1. When you click the "Click to throw ignored promise rejection" button, the overlay should not appear but you should see an error is logged in console (default browser behavior).

examples/client/overlay/app.js

+35-3
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,50 @@
11
"use strict";
22

33
// eslint-disable-next-line import/order
4-
const createErrorBtn = require("./error-button");
4+
const createButton = require("./create-button");
5+
6+
/**
7+
* @param {string} errorMessage
8+
*/
9+
function unsafeOperation(errorMessage) {
10+
throw new Error(errorMessage);
11+
}
512

613
const target = document.querySelector("#target");
714

815
target.insertAdjacentElement(
916
"afterend",
10-
createErrorBtn("Click to throw error", "Error message thrown from JS")
17+
createButton("Click to throw ignored promise rejection", () => {
18+
const abortController = new AbortController();
19+
20+
fetch("https://google.com", {
21+
signal: abortController.signal,
22+
mode: "no-cors",
23+
});
24+
25+
setTimeout(() => abortController.abort(), 100);
26+
})
27+
);
28+
29+
target.insertAdjacentElement(
30+
"afterend",
31+
createButton("Click to throw unhandled promise rejection", () => {
32+
setTimeout(() => Promise.reject(new Error("Async error")), 100);
33+
})
34+
);
35+
36+
target.insertAdjacentElement(
37+
"afterend",
38+
createButton("Click to throw ignored error", () => {
39+
unsafeOperation("something something");
40+
})
1141
);
1242

1343
target.insertAdjacentElement(
1444
"afterend",
15-
createErrorBtn("Click to throw ignored error", "something something")
45+
createButton("Click to throw error", () => {
46+
unsafeOperation("Error message thrown from JS");
47+
})
1648
);
1749

1850
// eslint-disable-next-line import/no-unresolved, import/extensions
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"use strict";
2+
3+
/**
4+
*
5+
* @param {string} label
6+
* @param {() => void} onClick
7+
* @returns HTMLButtonElement
8+
*/
9+
module.exports = function createButton(label, onClick) {
10+
const button = document.createElement("button");
11+
12+
button.addEventListener("click", onClick);
13+
button.innerHTML = label;
14+
15+
return button;
16+
};

examples/client/overlay/error-button.js

-24
This file was deleted.

examples/client/overlay/webpack.config.js

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ module.exports = setup({
1414
warnings: false,
1515
runtimeErrors: (msg) => {
1616
if (msg) {
17+
if (msg instanceof DOMException && msg.name === "AbortError") {
18+
return false;
19+
}
20+
1721
let msgString;
1822

1923
if (msg instanceof Error) {

test/e2e/__snapshots__/overlay.test.js.snap.webpack4

+84
Original file line numberDiff line numberDiff line change
@@ -2136,6 +2136,90 @@ exports[`overlay should show an error when "client.overlay.warnings" is "true":
21362136
"
21372137
`;
21382138

2139+
exports[`overlay should show error for uncaught promise rejection: overlay html 1`] = `
2140+
"<body>
2141+
<div
2142+
id=\\"webpack-dev-server-client-overlay-div\\"
2143+
style=\\"
2144+
position: fixed;
2145+
box-sizing: border-box;
2146+
inset: 0px;
2147+
width: 100vw;
2148+
height: 100vh;
2149+
font-size: large;
2150+
padding: 2rem 2rem 4rem;
2151+
line-height: 1.2;
2152+
white-space: pre-wrap;
2153+
overflow: auto;
2154+
background-color: rgba(0, 0, 0, 0.9);
2155+
color: white;
2156+
\\"
2157+
>
2158+
<div
2159+
style=\\"
2160+
color: rgb(232, 59, 70);
2161+
font-size: 2em;
2162+
white-space: pre-wrap;
2163+
font-family: sans-serif;
2164+
margin: 0px 2rem 2rem 0px;
2165+
flex: 0 0 auto;
2166+
max-height: 50%;
2167+
overflow: auto;
2168+
\\"
2169+
>
2170+
Uncaught runtime errors:
2171+
</div>
2172+
<button
2173+
aria-label=\\"Dismiss\\"
2174+
style=\\"
2175+
color: rgb(255, 255, 255);
2176+
line-height: 1rem;
2177+
font-size: 1.5rem;
2178+
padding: 1rem;
2179+
cursor: pointer;
2180+
position: absolute;
2181+
right: 0px;
2182+
top: 0px;
2183+
background-color: transparent;
2184+
border: none;
2185+
\\"
2186+
>
2187+
×
2188+
</button>
2189+
<div>
2190+
<div
2191+
style=\\"
2192+
background-color: rgba(206, 17, 38, 0.1);
2193+
color: rgb(252, 207, 207);
2194+
padding: 1rem 1rem 1.5rem;
2195+
\\"
2196+
>
2197+
<div
2198+
style=\\"
2199+
color: rgb(232, 59, 70);
2200+
font-size: 1.2em;
2201+
margin-bottom: 1rem;
2202+
font-family: sans-serif;
2203+
\\"
2204+
>
2205+
ERROR
2206+
</div>
2207+
<div
2208+
style=\\"
2209+
line-height: 1.5;
2210+
font-size: 1rem;
2211+
font-family: Menlo, Consolas, monospace;
2212+
\\"
2213+
>
2214+
Async error at &lt;anonymous&gt;:3:26
2215+
</div>
2216+
</div>
2217+
</div>
2218+
</div>
2219+
</body>
2220+
"
2221+
`;
2222+
21392223
exports[`overlay should show error for uncaught runtime error: overlay html 1`] = `
21402224
"<body>
21412225
<div

0 commit comments

Comments
 (0)