Inside a service worker, what does event.waitUntil(promise) do in the install and activate events, and what goes wrong if you start async work without it?
answer
- events, not processes; worker can be killed
- extends the event's lifetime
- holds the lifecycle state transition
- install rejection means never activates
- must be called synchronously in the handler
basics
~20 swaitUntil() extends the lifetime of a service worker event: it keeps the worker alive until the promise settles and holds the lifecycle at that stage. Without it the browser may treat install or activate as finished — and terminate the worker — while your async work is still running.
solid answer
~50 sA service worker is not a long-running process; the browser starts it to handle an event and is free to terminate it as soon as the handler returns. `event.waitUntil(promise)` tells the browser "this event is not really over yet": it keeps the worker alive and holds the lifecycle at that stage until the promise settles. In `install`, that means the worker does not become *installed* until your setup finishes, and a **rejected** promise fails the installation outright — the worker becomes redundant and the previously active worker keeps running. In `activate`, the worker is not considered activated until the promise settles, and functional events such as `fetch` are held back, so you can finish migration or cleanup before any request is served. Omit it and the browser sees a handler that returned immediately: it may kill the worker mid-task, and the lifecycle advances as if your setup succeeded even when it silently did not.
code
javascript · 14 linesself.addEventListener('install', (event) => {
console.log('install: start');
event.waitUntil(
(async () => {
await new Promise((resolve) => setTimeout(resolve, 3000));
console.log('install: setup done');
})()
);
});
self.addEventListener('activate', (event) => {
// Runs only after the install promise above has settled.
console.log('activate: install had already finished');
});go deeper
Know that a service worker can be shut down between events, and that waitUntil is how you tell the browser your install or activate work is still running.
Explain both effects precisely: the worker is kept alive, and the state transition is held — plus the fact that a rejected install promise discards the new worker and leaves the old one active.
Demonstrate the failure mode: setup started without waitUntil finishes on a fast laptop and gets killed on a slow phone, producing a small percentage of users with a broken worker. Argue for keeping install work minimal and required-only.
Own the atomicity contract for deploys: what belongs on the install critical path, what an install failure should mean for a release, and how you detect that a fraction of users are stuck on the previous worker.
## Service workers are event-driven and disposable The first thing to internalise is that a service worker has no guaranteed lifetime. The browser spins it up when there is an event to deliver, and once the event handlers have returned it is entitled to shut the worker down — it may do so within seconds to save memory. Any `async` work you kicked off and did not declare is at risk of being cut off mid-flight, and global variables you set do not survive the restart. `ExtendableEvent.waitUntil(promise)` is the declaration. It is available on the lifecycle events (`install`, `activate`) and on functional events derived from `ExtendableEvent` such as `push` and `sync`. The related method on `FetchEvent` is `respondWith()`, which extends the event's lifetime as well as supplying the response. ## In the install event ```js self.addEventListener('install', (event) => { event.waitUntil(setup()); }); ``` Three things follow: 1. **The worker stays alive** until `setup()` settles. 2. **The state transition waits.** The worker stays *installing* and only becomes *installed* when the promise fulfils. 3. **Rejection fails the install.** If the promise rejects, the installation is abandoned: the new worker becomes *redundant* and never activates. Crucially, the previously active worker is untouched and keeps controlling pages. This is the mechanism that makes installation atomic — you can do your setup work knowing that a partial failure will not leave a half-ready worker in charge. That last property is why setup work belongs in `install` behind `waitUntil` rather than being fired off and forgotten. "Fetch all the things I need, and if any of it fails, don't become the app's worker" is exactly the guarantee you want. ## In the activate event ```js self.addEventListener('activate', (event) => { event.waitUntil(migrate()); }); ``` During activation the worker is in the *activating* state. Functional events are not dispatched to it while `waitUntil` is pending — the browser holds them until the promise settles and the worker reaches *activated*. That is the window in which it is safe to do cleanup or data migration that would confuse a request handler if it ran concurrently. Note the asymmetry with install: a rejected `activate` promise does **not** roll the worker back; it activates anyway. `waitUntil` in `activate` buys you ordering, not atomicity. ## What omitting it looks like ```js // Bug: nothing is waited on self.addEventListener('install', async () => { await slowSetup(); // may never finish }); ``` An `async` handler returns a promise, but the event does not look at the handler's return value — only at what you pass to `waitUntil`. So the browser sees an empty handler, marks the worker installed, and is free to terminate it. On a fast desktop connection the work usually completes and the bug hides; on a slow phone the worker gets killed mid-setup and activates in a broken state. The symptom is the classic "works on my machine, broken for 3% of users". ## Practical rules - Pass **one** promise. Multiple `waitUntil` calls are allowed and are all awaited, but the common shape is a single async IIFE: ```js event.waitUntil((async () => { … })()); ``` - Call it **synchronously** inside the handler. Calling `waitUntil` after an `await` — once the event has already finished dispatching — throws an `InvalidStateError`, because by then the extension window has closed. - Do not treat it as "keep my worker alive forever". Browsers impose their own cap on how long an event may be extended and will terminate a worker that overruns it, so `waitUntil` is for bounded setup work, not for background loops. - Keep install work minimal and required-only. Everything in the `waitUntil` promise is on the critical path to the new worker becoming usable, and every optional fetch you add is another way for the install to fail.
- If the install promise rejects, what happens to the worker that was already controlling pages?Nothing — it stays active and keeps controlling its clients. The failed candidate transitions to `redundant` and is discarded. That is the point of doing setup inside `waitUntil`: installation is all-or-nothing, so a failed deploy leaves the previous working version in charge rather than promoting a half-initialised worker.
- Does a rejected promise in the activate event roll the activation back?No. Activation is not atomic the way installation is: the worker becomes activated regardless of whether the `waitUntil` promise fulfils or rejects. What `waitUntil` gives you in `activate` is ordering — functional events like `fetch` are withheld until it settles — so it is the right place for migrations, but you must handle failures yourself.
- Why does calling event.waitUntil() after an await throw?The extension window is only open while the event is being dispatched. Once your handler has yielded and the dispatch has completed, the event is no longer extendable and `waitUntil` throws `InvalidStateError`. The fix is to build the whole promise chain synchronously — typically `event.waitUntil((async () => { … })())` — so the call happens before any await.
saying these in an interview costs you the question
- Thinks an async handler is awaited without waitUntil
- Believes waitUntil keeps the worker alive indefinitely
- Says a rejected activate promise rolls activation back
- Calls waitUntil after an await and expects it to work
- Confuses waitUntil with respondWith on fetch events