Skip to content

Conversation

@andriivitiv
Copy link
Contributor

Fixes #11595, where cancelling loading with loadingTask.destroy() before it finishes throws a Worker was terminated error that CANNOT be caught.

When worker is terminated, an error is thrown here:

throw new Error("Worker was terminated");

Then onFailure runs, in which we throw again via ensureNotTerminated(). However, this second error is never caught (and cannot be), resulting in console spam.

There is no need to throw any additional errors since the termination is already reported here, and onFailure is supposed to handle errors, not throw them.

Fixes mozilla#11595, where cancelling loading with `loadingTask.destroy()` before it finishes throws a `Worker was terminated` error that CANNOT be caught.

When worker is terminated, an error is thrown here:

https://github.com/mozilla/pdf.js/blob/6c746260a98766b8ece27018d2c48436cfcafa24/src/core/worker.js#L374

Then `onFailure` runs, in which we throw again via `ensureNotTerminated()`. However, this second error is never caught (and cannot be), resulting in console spam.

There is no need to throw any additional errors since the termination is already reported [here](https://github.com/mozilla/pdf.js/blob/6c746260a98766b8ece27018d2c48436cfcafa24/src/core/worker.js#L371-L373), and `onFailure` is supposed to handle errors, not throw them.
@calixteman
Copy link
Contributor

Is it possible to write a test ?

@andriivitiv
Copy link
Contributor Author

Is it possible to write a test ?

@calixteman I think the closest way would be something like the following:

Code
function makeAsyncCallback() {
  let resolve;

  const promise = new Promise(r => {
    resolve = r;
  });

  const func = function () {
    resolve();
  };

  return { func, promise };
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function onUnhandledRejection(event) {
  fail(event.reason || event);
}



it("no unhandled error is thrown when loading is cancelled", async function () {
  if (isNodeJS) {
    pending("Worker is not supported in Node.js.");
  }

  const { func: onProgress, promise: waitForProgress } = makeAsyncCallback();

  window.addEventListener("unhandledrejection", onUnhandledRejection);

  try {
    const loadingTask = getDocument(basicApiGetDocumentParams);
    loadingTask.onProgress = onProgress;

    await waitForProgress;
    await loadingTask.destroy();

    // There's probably a better way to wait a bit.
    await sleep(1000);
  } finally {
    window.removeEventListener("unhandledrejection", onUnhandledRejection);
  }
});

However, it still doesn’t work because triggering this error requires very specific timing. For example, I created a simple reproduction by modifying this file, and I can only get this error when throttling is enabled (probably because the PDF is too small). So i think it won't be possible to write a reliable test.

Code
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>'Hello, world!' example</title>
  </head>
  <body>
    <h1>'Hello, world!' example</h1>

    <button id="reload">Reload</button>
    <br /><br />

    <canvas id="the-canvas" style="border: 1px solid black; direction: ltr"></canvas>

    <script src="../../build/generic/build/pdf.mjs" type="module"></script>

    <script id="script" type="module">
      pdfjsLib.GlobalWorkerOptions.workerSrc = "../../build/generic/build/pdf.worker.mjs";

      const url = "./helloworld.pdf";

      const canvas = document.getElementById("the-canvas");
      const ctx = canvas.getContext("2d");

      let loadingTask = null;

      async function loadDocument() {
        if (loadingTask) {
          await loadingTask.destroy();
          loadingTask = null;
        }

        loadingTask = pdfjsLib.getDocument({url});

        try {
          const pdf = await loadingTask.promise;
          const page = await pdf.getPage(1);

          const scale = 1.5;
          const viewport = page.getViewport({ scale });
          const dpr = window.devicePixelRatio || 1;

          canvas.width = viewport.width * dpr;
          canvas.height = viewport.height * dpr;
          canvas.style.width = viewport.width + "px";
          canvas.style.height = viewport.height + "px";

          const transform = dpr !== 1 ? [dpr, 0, 0, dpr, 0, 0] : null;

          page.render({
            canvasContext: ctx,
            viewport,
            transform,
          }).promise;
        } catch (err) {
            if (loadingTask?.destroyed) return;
            console.error(err);
        }
      }

      // initial load
      loadDocument();

      document.getElementById("reload").onclick = loadDocument;
    </script>

    <hr />
    <h2>JavaScript code:</h2>
    <pre id="code"></pre>
    <script>
      document.getElementById("code").textContent =
        document.getElementById("script").text;
    </script>
  </body>
</html>
Screen.Recording.2026-01-09.at.21.35.16.mov

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

loadingTask.destroy() throws worker exception

3 participants