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

Diff for: 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

Diff for: 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 };

Diff for: 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).

Diff for: 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

Diff for: examples/client/overlay/create-button.js

+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+
};

Diff for: examples/client/overlay/error-button.js

-24
This file was deleted.

Diff for: 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) {

Diff for: 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)