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

Unhandled rejections in workers should not fire an error event on the Worker object #12221

Open
andreubotella opened this issue Sep 25, 2021 · 10 comments
Labels
bug Something isn't working correctly web related to Web APIs

Comments

@andreubotella
Copy link
Contributor

andreubotella commented Sep 25, 2021

The HTML spec defines steps for reporting runtime script errors (with the error event) and unhandled promise rejections (with the unhandledrejection event). Both mechanisms are fairly similar, but they differ on what happens when an error or rejection happens inside a dedicated worker and is not caught by the corresponding event. Unhandled promise rejections will simply be reported to the console, as they would in a window agent, and never make it outside of the worker – while script errors will fire an error event to the corresponding Worker object in the parent agent.

Deno doesn't (yet?) implement the unhandledrejection event, and an unhandled rejection on the main thread will kill the process. However, an unhandled rejection in a worker will instead fire an error event on the corresponding Worker object – which shouldn't happen per the spec. Arguably the process should be killed, as it would if it happened on the main thread.

This is causing some worker tests to hang forever in #12222.


Here's a test to compare the behavior in Deno and in browsers.

// main.js
const worker = new Worker(new URL("./worker.js", import.meta.url), {type: "module"});

worker.addEventListener("error", () => {
    console.log("worker.onerror in main thread");
});

window.addEventListener("error", () => {
    console.log("window.onerror in main thread");
});

window.addEventListener("unhandledrejection", () => {
    console.log("window.onunhandledrejection in main thread");
});
// worker.js
self.addEventListener("error", () => {
    console.log("self.onerror in worker");
});

self.addEventListener("unhandledrejection", () => {
    console.log("self.onunhandledrejection in worker");
});

(async () => {
    throw new Error("Error");
})();

All browsers fire the unhandledrejection event inside the worker, report the error, and don't fire any event in the main thread:
image

While Deno will report the error inside the worker, fire the error event on the main thread's worker object, and then die because the error wasn't handled:
image

(This screenshot uses Deno canary – 1.14.1 would panic because of #11342.)

andreubotella pushed a commit to andreubotella/deno that referenced this issue Sep 25, 2021
Classic workers were implemented in denoland#11338, which also enabled the WPT
tests in the `workers` directory. However, the rest of WPT worker tests
were not enabled because a number of them were hanging due to
web-platform-tests/wpt#29777. Now that that WPT issue is fixed, the bulk
of worker tests can be enabled.

There are still a few tests that hang, and so haven't been enabled. In
particular:

- The following tests seem to hang because a promise fails to resolve.
  We can detect such cases in non-worker tests because the process will
  exit without calling the WPT completion callback, but in worker tests
  the worker message ops will keep the event loop running. This will be
  fixed when we add timeouts to WPT tests (denoland#9460).

  - `/fetch/api/basic/error-after-response.any.worker.html`
  - `/html/webappapis/microtask-queuing/queue-microtask-exceptions.any.worker.html`
  - `/webmessaging/message-channels/worker-post-after-close.any.worker.html`
  - `/webmessaging/message-channels/worker.any.worker.html`
  - `/websockets/Create-on-worker-shutdown.any.worker.html`

- The following tests apparently hang because a promise rejection is
  never handled, which will kill the process in the main thread but not
  in workers (denoland#12221).

  - `/streams/readable-streams/async-iterator.any.worker.html`
  - `/workers/interfaces/WorkerUtils/importScripts/report-error-setTimeout-cross-origin.sub.any.worker.html`
  - `/workers/interfaces/WorkerUtils/importScripts/report-error-setTimeout-redirect-to-cross-origin.sub.any.worker.html`
  - `/workers/interfaces/WorkerUtils/importScripts/report-error-setTimeout-same-origin.sub.any.worker.html`
@nayeemrmn
Copy link
Collaborator

Browsers on unhandled errors in workers will:

  1. Print the error.
  2. Dispatch an error event within the worker ("error" for sync and "unhandledrejection" for async).
  3. Propagate an "error" event up to the Worker object in the host, only for sync.
  4. Allow worker code not blocked on the error site to continue.

Deno on unhanded errors in workers will:

  1. Print the error.
  2. Kill the worker, no events are dispatched within the worker.
  3. Propagate an "error" event up to the Worker object in the host, for both sync and async.
  4. Not allow any other worker code to continue, the worker has been killed.

Most differences are intentional, but we can improve points 2 and 3.

  1. Print the error.
  2. Kill the worker, but first dispatch the proper events and allow one more event loop for listeners to run.
  3. Propagate an "error" event up to the Worker object in the host, only for sync. Kill the process on async errors in workers.
    • As you suggested. Since async errors are not supposed to be passed up to the host, killing the entire process on them is the only safe thing left to do. It would encourage handling async errors same as normal.
  4. Not allow any other worker code to continue, the worker has been killed.

@andreubotella
Copy link
Contributor Author

andreubotella commented Sep 25, 2021

That SGTM.

  1. Kill the worker, but first dispatch the proper events and allow one more event loop for listeners to run.

Having the process exit at some point after the async error is thrown is enough to prevent the hangs. (The real cause of the hangs is that WPT will not try to use setTimeout in non-browser environments, so tests won't time out – and in some tests, a promise rejection in the runtime Deno code seems to be preventing other promises from resolving. In non-worker tests, the unhandled rejection will kill the process, but in worker tests it won't.)

@kevinkassimo
Copy link
Contributor

kevinkassimo commented Sep 26, 2021

  1. Kill the worker, but first dispatch the proper events and allow one more event loop for listeners to run.

Experimented with dispatching an error event for sync errors (so that it works more similar to browser) in #12242 . There seems to be some issues wrt to main worker exiting too early. Essentially due to

if (globalThis instanceof Window) {
throw new Error("Unhandled error event reached main worker.");

As the main thread exits under error, the handler code in the worker would simply die abruptly halfway since its thread handle is getting dropped along with it and cause the isolate to terminate. I wonder if blocking the main worker until the sync error handler code in the target worker is completed is acceptable by any chance?

andreubotella pushed a commit to andreubotella/deno that referenced this issue Oct 7, 2021
Classic workers were implemented in denoland#11338, which also enabled the WPT
tests in the `workers` directory. However, the rest of WPT worker tests
were not enabled because a number of them were hanging due to
web-platform-tests/wpt#29777. Now that that WPT issue is fixed, the bulk
of worker tests can be enabled.

There are still a few tests that hang, and so haven't been enabled. In
particular:

- The following tests seem to hang because a promise fails to resolve.
  We can detect such cases in non-worker tests because the process will
  exit without calling the WPT completion callback, but in worker tests
  the worker message ops will keep the event loop running. This will be
  fixed when we add timeouts to WPT tests (denoland#9460).

  - `/fetch/api/basic/error-after-response.any.worker.html`
  - `/html/webappapis/microtask-queuing/queue-microtask-exceptions.any.worker.html`
  - `/webmessaging/message-channels/worker-post-after-close.any.worker.html`
  - `/webmessaging/message-channels/worker.any.worker.html`
  - `/websockets/Create-on-worker-shutdown.any.worker.html`

- The following tests apparently hang because a promise rejection is
  never handled, which will kill the process in the main thread but not
  in workers (denoland#12221).

  - `/streams/readable-streams/async-iterator.any.worker.html`
  - `/workers/interfaces/WorkerUtils/importScripts/report-error-setTimeout-cross-origin.sub.any.worker.html`
  - `/workers/interfaces/WorkerUtils/importScripts/report-error-setTimeout-redirect-to-cross-origin.sub.any.worker.html`
  - `/workers/interfaces/WorkerUtils/importScripts/report-error-setTimeout-same-origin.sub.any.worker.html`
bartlomieju pushed a commit that referenced this issue Oct 8, 2021
Classic workers were implemented in #11338, which also enabled the WPT
tests in the `workers` directory. However, the rest of WPT worker tests
were not enabled because a number of them were hanging due to
web-platform-tests/wpt#29777. Now that that WPT issue is fixed, the bulk
of worker tests can be enabled.

There are still a few tests that hang, and so haven't been enabled. In
particular:

- The following tests seem to hang because a promise fails to resolve.
  We can detect such cases in non-worker tests because the process will
  exit without calling the WPT completion callback, but in worker tests
  the worker message ops will keep the event loop running. This will be
  fixed when we add timeouts to WPT tests (#9460).

  - `/fetch/api/basic/error-after-response.any.worker.html`
  - `/html/webappapis/microtask-queuing/queue-microtask-exceptions.any.worker.html`
  - `/webmessaging/message-channels/worker-post-after-close.any.worker.html`
  - `/webmessaging/message-channels/worker.any.worker.html`
  - `/websockets/Create-on-worker-shutdown.any.worker.html`

- The following tests apparently hang because a promise rejection is
  never handled, which will kill the process in the main thread but not
  in workers (#12221).

  - `/streams/readable-streams/async-iterator.any.worker.html`
  - `/workers/interfaces/WorkerUtils/importScripts/report-error-setTimeout-cross-origin.sub.any.worker.html`
  - `/workers/interfaces/WorkerUtils/importScripts/report-error-setTimeout-redirect-to-cross-origin.sub.any.worker.html`
  - `/workers/interfaces/WorkerUtils/importScripts/report-error-setTimeout-same-origin.sub.any.worker.html`
bartlomieju pushed a commit to bartlomieju/deno that referenced this issue Oct 10, 2021
Classic workers were implemented in denoland#11338, which also enabled the WPT
tests in the `workers` directory. However, the rest of WPT worker tests
were not enabled because a number of them were hanging due to
web-platform-tests/wpt#29777. Now that that WPT issue is fixed, the bulk
of worker tests can be enabled.

There are still a few tests that hang, and so haven't been enabled. In
particular:

- The following tests seem to hang because a promise fails to resolve.
  We can detect such cases in non-worker tests because the process will
  exit without calling the WPT completion callback, but in worker tests
  the worker message ops will keep the event loop running. This will be
  fixed when we add timeouts to WPT tests (denoland#9460).

  - `/fetch/api/basic/error-after-response.any.worker.html`
  - `/html/webappapis/microtask-queuing/queue-microtask-exceptions.any.worker.html`
  - `/webmessaging/message-channels/worker-post-after-close.any.worker.html`
  - `/webmessaging/message-channels/worker.any.worker.html`
  - `/websockets/Create-on-worker-shutdown.any.worker.html`

- The following tests apparently hang because a promise rejection is
  never handled, which will kill the process in the main thread but not
  in workers (denoland#12221).

  - `/streams/readable-streams/async-iterator.any.worker.html`
  - `/workers/interfaces/WorkerUtils/importScripts/report-error-setTimeout-cross-origin.sub.any.worker.html`
  - `/workers/interfaces/WorkerUtils/importScripts/report-error-setTimeout-redirect-to-cross-origin.sub.any.worker.html`
  - `/workers/interfaces/WorkerUtils/importScripts/report-error-setTimeout-same-origin.sub.any.worker.html`
@stale
Copy link

stale bot commented Nov 26, 2021

This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Nov 26, 2021
@bartlomieju bartlomieju removed the stale label Nov 26, 2021
@stale
Copy link

stale bot commented Jan 25, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Jan 25, 2022
@kitsonk kitsonk added bug Something isn't working correctly web related to Web APIs labels Jan 25, 2022
@stale stale bot removed the stale label Jan 25, 2022
@Mutefish0
Copy link
Contributor

Is there any update ? Suffering from the same issue.

@nakasyou
Copy link

nakasyou commented Oct 1, 2023

I have the same problem.

@yacinehmito
Copy link
Contributor

@nayeemrmn You wrote "most differences are intentional". What is the rationale behind those differences?

@nayeemrmn
Copy link
Collaborator

You wrote "most differences are intentional". What is the rationale behind those differences?

Browsers won't close tabs on JS errors for example, but for a backend runtime we want to exit on errors as is standard

@josephrocca
Copy link
Contributor

josephrocca commented Oct 12, 2024

This issue might be about something more complicated than what I'm dealing with, but in case it's helpful to others, here's a workaround to prevent unhandledrejection from propagating to the worker.onerror handler in the parent:

// in worker script:
self.addEventListener("unhandledrejection", (event) => {
  console.error("Unhandled rejection:", event.reason);
  event.preventDefault();
  self.close();
});

The event.preventDefault() call doesn't seem to be needed, but it does prevent the error from propagating to the parent when used without self.close(), so I'm leaving it just in case self.close() behavior is not well defined in terms of ordering of the relevant operations here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working correctly web related to Web APIs
Projects
None yet
Development

No branches or pull requests

9 participants