Skip to content

Commit

Permalink
feat: overlay displays unhandled promise rejection (#4849)
Browse files Browse the repository at this point in the history
  • Loading branch information
malcolm-kee committed May 7, 2023
1 parent 51f8a1b commit d1dd430
Show file tree
Hide file tree
Showing 10 changed files with 395 additions and 37 deletions.
33 changes: 24 additions & 9 deletions client-src/overlay.js
Expand Up @@ -5,6 +5,7 @@ import ansiHTML from "ansi-html-community";
import { encode } from "html-entities";
import {
listenToRuntimeError,
listenToUnhandledRejection,
parseErrorToStacks,
} from "./overlay/runtime-error.js";
import createOverlayMachine from "./overlay/state-machine.js";
Expand Down Expand Up @@ -282,16 +283,13 @@ const createOverlay = (options) => {
});

if (options.catchRuntimeError) {
listenToRuntimeError((errorEvent) => {
// error property may be empty in older browser like IE
const { error, message } = errorEvent;

if (!error && !message) {
return;
}

/**
* @param {Error | undefined} error
* @param {string} fallbackMessage
*/
const handleError = (error, fallbackMessage) => {
const errorObject =
error instanceof Error ? error : new Error(error || message);
error instanceof Error ? error : new Error(error || fallbackMessage);

const shouldDisplay =
typeof options.catchRuntimeError === "function"
Expand All @@ -309,6 +307,23 @@ const createOverlay = (options) => {
],
});
}
};

listenToRuntimeError((errorEvent) => {
// error property may be empty in older browser like IE
const { error, message } = errorEvent;

if (!error && !message) {
return;
}

handleError(error, message);
});

listenToUnhandledRejection((promiseRejectionEvent) => {
const { reason } = promiseRejectionEvent;

handleError(reason, "Unknown promise rejection reason");
});
}

Expand Down
19 changes: 18 additions & 1 deletion client-src/overlay/runtime-error.js
Expand Up @@ -30,4 +30,21 @@ function listenToRuntimeError(callback) {
};
}

export { listenToRuntimeError, parseErrorToStacks };
/**
* @callback UnhandledRejectionCallback
* @param {PromiseRejectionEvent} rejectionEvent
* @returns {void}
*/

/**
* @param {UnhandledRejectionCallback} callback
*/
function listenToUnhandledRejection(callback) {
window.addEventListener("unhandledrejection", callback);

return function cleanup() {
window.removeEventListener("unhandledrejection", callback);
};
}

export { listenToRuntimeError, listenToUnhandledRejection, parseErrorToStacks };
51 changes: 51 additions & 0 deletions examples/client/overlay/README.md
Expand Up @@ -31,3 +31,54 @@ npx webpack serve --open --no-client-overlay
2. You should see an overlay in browser for compilation errors.
3. Update `entry` in webpack.config.js to `app.js` and save.
4. You should see the text on the page itself change to read `Success!`.

## Additional Configurations

### Filter errors by function

**webpack.config.js**

```js
module.exports = {
devServer: {
client: {
overlay: {
runtimeErrors: (msg) => {
if (msg) {
if (msg instanceof DOMException && msg.name === "AbortError") {
return false;
}

let msgString;

if (msg instanceof Error) {
msgString = msg.message;
} else if (typeof msg === "string") {
msgString = msg;
}

if (msgString) {
return !/something/i.test(msgString);
}
}

return true;
},
},
},
},
};
```

Run the command:

```shell
npx webpack serve --open
```

What should happens:

1. When you click the "Click to throw error" button, the overlay should appears.
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).
1. When you click the "Click to throw unhandled promise rejection" button, the overlay should appears.
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).
38 changes: 35 additions & 3 deletions examples/client/overlay/app.js
@@ -1,18 +1,50 @@
"use strict";

// eslint-disable-next-line import/order
const createErrorBtn = require("./error-button");
const createButton = require("./create-button");

/**
* @param {string} errorMessage
*/
function unsafeOperation(errorMessage) {
throw new Error(errorMessage);
}

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

target.insertAdjacentElement(
"afterend",
createErrorBtn("Click to throw error", "Error message thrown from JS")
createButton("Click to throw ignored promise rejection", () => {
const abortController = new AbortController();

fetch("https://google.com", {
signal: abortController.signal,
mode: "no-cors",
});

setTimeout(() => abortController.abort(), 100);
})
);

target.insertAdjacentElement(
"afterend",
createButton("Click to throw unhandled promise rejection", () => {
setTimeout(() => Promise.reject(new Error("Async error")), 100);
})
);

target.insertAdjacentElement(
"afterend",
createButton("Click to throw ignored error", () => {
unsafeOperation("something something");
})
);

target.insertAdjacentElement(
"afterend",
createErrorBtn("Click to throw ignored error", "something something")
createButton("Click to throw error", () => {
unsafeOperation("Error message thrown from JS");
})
);

// eslint-disable-next-line import/no-unresolved, import/extensions
Expand Down
16 changes: 16 additions & 0 deletions examples/client/overlay/create-button.js
@@ -0,0 +1,16 @@
"use strict";

/**
*
* @param {string} label
* @param {() => void} onClick
* @returns HTMLButtonElement
*/
module.exports = function createButton(label, onClick) {
const button = document.createElement("button");

button.addEventListener("click", onClick);
button.innerHTML = label;

return button;
};
24 changes: 0 additions & 24 deletions examples/client/overlay/error-button.js

This file was deleted.

4 changes: 4 additions & 0 deletions examples/client/overlay/webpack.config.js
Expand Up @@ -14,6 +14,10 @@ module.exports = setup({
warnings: false,
runtimeErrors: (msg) => {
if (msg) {
if (msg instanceof DOMException && msg.name === "AbortError") {
return false;
}

let msgString;

if (msg instanceof Error) {
Expand Down
84 changes: 84 additions & 0 deletions test/e2e/__snapshots__/overlay.test.js.snap.webpack4
Expand Up @@ -2136,6 +2136,90 @@ exports[`overlay should show an error when "client.overlay.warnings" is "true":
"
`;

exports[`overlay should show error for uncaught promise rejection: overlay html 1`] = `
"<body>
<div
id=\\"webpack-dev-server-client-overlay-div\\"
style=\\"
position: fixed;
box-sizing: border-box;
inset: 0px;
width: 100vw;
height: 100vh;
font-size: large;
padding: 2rem 2rem 4rem;
line-height: 1.2;
white-space: pre-wrap;
overflow: auto;
background-color: rgba(0, 0, 0, 0.9);
color: white;
\\"
>
<div
style=\\"
color: rgb(232, 59, 70);
font-size: 2em;
white-space: pre-wrap;
font-family: sans-serif;
margin: 0px 2rem 2rem 0px;
flex: 0 0 auto;
max-height: 50%;
overflow: auto;
\\"
>
Uncaught runtime errors:
</div>
<button
aria-label=\\"Dismiss\\"
style=\\"
color: rgb(255, 255, 255);
line-height: 1rem;
font-size: 1.5rem;
padding: 1rem;
cursor: pointer;
position: absolute;
right: 0px;
top: 0px;
background-color: transparent;
border: none;
\\"
>
×
</button>
<div>
<div
style=\\"
background-color: rgba(206, 17, 38, 0.1);
color: rgb(252, 207, 207);
padding: 1rem 1rem 1.5rem;
\\"
>
<div
style=\\"
color: rgb(232, 59, 70);
font-size: 1.2em;
margin-bottom: 1rem;
font-family: sans-serif;
\\"
>
ERROR
</div>
<div
style=\\"
line-height: 1.5;
font-size: 1rem;
font-family: Menlo, Consolas, monospace;
\\"
>
Async error at &lt;anonymous&gt;:3:26
</div>
</div>
</div>
</div>
</body>
"
`;

exports[`overlay should show error for uncaught runtime error: overlay html 1`] = `
"<body>
<div
Expand Down

0 comments on commit d1dd430

Please sign in to comment.