skip to content

In the browser Worker API, what is the difference between calling `worker.terminate()` from the page and calling `self.close()` inside the worker, and what happens to work that is still in flight?

level: middleimportance: should knowfreq 45%

answer

  1. one call from outside, one from inside
  2. abrupt versus cooperative
  3. no finally block on the abrupt one
  4. close() is not return
  5. forgetting both is the real bug

basics

~20 s

terminate() is the page killing the worker immediately from outside: the running script is aborted mid-execution and no cleanup runs. self.close() is the worker retiring itself: it discards its queued tasks and accepts no more, but the statements after the call still finish.

solid answer

~60 s

`worker.terminate()` is called on the page's `Worker` handle and stops the thread abruptly from outside. Whatever the worker was executing is cut off wherever it happens to be — no `finally` block runs, no result is delivered, and the page gets no event telling it this happened; the handle is simply dead afterwards. `self.close()` is the worker deciding it is finished. It sets the worker's closing flag: queued tasks are discarded and no new ones — timers, incoming messages, network callbacks — will be run. But it is not `return`: the statements after `close()` in the same turn still execute, so a worker can post a final result and then close in that order. The practical rule is that `close()` is the cooperative shutdown you use when the worker knows its job is done, and `terminate()` is the page's kill switch for a worker that is stuck, no longer needed, or running work the user just cancelled. Either way the thread's memory is reclaimed; neither can be undone, and a new `Worker` must be constructed to start again.

code

javascript · 7 lines
javascript
// one-shot-worker.js
self.onmessage = (event) => {
  const result = event.data.reduce((a, b) => a + b, 0);
  self.postMessage(result); // queued to the page before shutdown
  self.close();             // no further tasks will be dispatched
  console.log('still runs: close() is not return');
};

go deeper

for a junior

Know that terminate() is called by the page on the Worker object and close() is called by the worker on itself, and that either way the worker cannot be restarted — you construct a new one.

for a middle

Explain the mechanics: terminate() aborts the running script with no cleanup and drops queued messages, while close() sets the closing flag so no further tasks run yet lets the current turn finish. Say why 'post result, then close' works.

for a senior

Show the operational angle: the leak from spawning workers per interaction and never tearing them down, a watchdog that terminates a wedged worker, and why cancellation must be designed for rather than bolted on.

for a principal

Own the lifecycle contract for the codebase — who is allowed to spawn workers, whether they are long-lived pooled resources or per-feature, and how cancellation and teardown are made impossible to forget rather than left to reviewer discipline.

## Two ends of the same rope A dedicated worker's lifetime is owned jointly. The page holds a `Worker` object and can pull the plug; the worker holds its own global and can retire itself. There are correspondingly two shutdown APIs, and they are not interchangeable. ## terminate() — the external kill ```js const worker = new Worker(new URL('./search.js', import.meta.url), { type: 'module' }); // ...later, the user navigated away from the search panel worker.terminate(); ``` `terminate()` aborts the worker immediately. Concretely: - The currently running script stops wherever the browser can stop it. It does not run to the end of the statement list. - No `finally` blocks, no `unload`-style hook, no destructor. Anything you were going to flush is lost. - Queued messages in both directions are dropped. A result the worker had already posted but the page had not yet dispatched will not arrive. - The page receives **no event**. `terminate()` returns `undefined` synchronously and there is no `terminated` callback to observe. - The `Worker` object is now inert. Calling `postMessage()` on it does nothing useful, and there is no restart — construct a new `Worker` instead. That abruptness is the point: it is the tool for a worker that has wedged itself in an infinite loop, or for cancelling expensive work whose result nobody wants any more. Because a worker cannot be interrupted cooperatively while it is inside a long synchronous computation — it is not checking a flag, it is busy — `terminate()` is often the *only* way to cancel that computation. ## self.close() — the cooperative retirement ```js // worker.js self.onmessage = (event) => { const result = expensiveTransform(event.data); self.postMessage(result); // deliver first self.close(); // then retire console.log('this line still runs'); }; ``` `close()` sets the worker's closing flag. From that moment the worker's event loop discards its queued tasks and refuses new ones: pending `setTimeout` callbacks will not fire, further `message` events are not dispatched, in-flight `fetch` responses are not delivered. What it does *not* do is stop the current turn. The statements after `close()` in the same function still execute — it behaves nothing like `return` or `process.exit`. That is useful, because it means the natural ordering "post the result, then close" works: the message has already been queued to the page and is unaffected by the worker's own shutdown. A worker that closes itself cannot be revived. The page's `Worker` object stays around but is dead; there is no event telling the page that it happened, so if the page needs to know it must be told by an ordinary message just before the close. ## Leaks: the case nobody calls either The most common real-world bug is neither API — it is forgetting both. A worker is not garbage collected merely because your code has stopped using it: as long as the page holds a reference and the worker has not closed, the thread and its heap stay alive. Patterns that spawn a worker per interaction and never terminate it accumulate threads, each with its own JS heap, until the tab is noticeably fat and the machine is oversubscribed. The two safe shapes are a **long-lived worker** you create once and keep for the feature's lifetime, and a **short-lived worker** that is explicitly torn down on the same lifecycle event that created it: ```js let worker = null; function openPanel() { worker = new Worker(new URL('./index.js', import.meta.url), { type: 'module' }); } function closePanel() { worker?.terminate(); worker = null; } ``` Dedicated workers are tied to their owning document, so a full page navigation tears them down for you — but a single-page app that never navigates gets no such rescue. ## Choosing between them Use `close()` when the worker is the one that knows it is finished: a one-shot job worker, or a worker that has hit an unrecoverable internal error and wants to stop cleanly after reporting it. Use `terminate()` when the *page* is the one that knows: the feature was closed, the user cancelled, a newer request superseded the old one, or a watchdog decided the worker is stuck. If you find yourself wanting `terminate()` mid-computation regularly, that is a hint the job should be split so the worker can also notice cancellation between chunks — but keep `terminate()` as the backstop, because a genuinely runaway loop will never notice anything.

  • Does the page get any event when a worker calls self.close()?
    No. There is no `close` or `terminated` event on the `Worker` object, and no error either — the handle just stops producing messages. If the page needs to know, the worker must post an ordinary message saying so immediately before calling `close()`, since messages already queued are delivered regardless of the shutdown.
  • A worker is stuck in an infinite synchronous loop. Can you cancel it without terminate()?
    No. A synchronous loop never yields to the worker's event loop, so it will never see a cancellation message or check a flag — the message sits in a queue that is not being drained. `terminate()` is the only way out, which is why long jobs are usually written to return control between chunks so cheaper cancellation is possible.
  • If a page holds a Worker reference it never uses again, will the browser eventually clean the thread up?
    Not while the document is alive. A live reference plus an un-closed worker keeps the thread and its heap resident, so per-interaction workers that are never terminated accumulate. A full page navigation destroys a document's dedicated workers, but a single-page app never triggers that, so teardown must be explicit.

saying these in an interview costs you the question

  • Thinks terminate() lets the worker finish cleanly first
  • Expects a finally block to run on terminate()
  • Treats self.close() as an immediate return statement
  • Waits for a 'terminated' event on the Worker object
  • Assumes an unused worker is garbage collected automatically

context