Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: overlay displays unhandled promise rejection #4849

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions client-src/overlay.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading