Handling push events in a service worker
Once a user is subscribed and a push message has been sent, the next step is receiving that message on the user's device and doing something with it—typically showing a notification, but any other background work can be done as well.
Listening for the push event
When a message arrives, the browser dispatches a push event to your service worker. Setting up a listener looks like any other JavaScript event listener:
self.addEventListener('push', function(event) {
if (event.data) {
console.log('This push event has data: ', event.data.text());
} else {
console.log('This push event has no data.');
}
});
The self variable is the first thing that tends to confuse developers new to service workers. self is used across Web Workers to reference the global scope. In a web page you'd use window, but inside a service worker self refers to the worker itself. So self.addEventListener() is simply attaching an event listener to the service worker.
The example above only checks for incoming data and logs it. To extract the payload, you can parse the event's data in a few different ways:
// Returns string
event.data.text()
// Parses data as JSON string and returns an Object
event.data.json()
// Returns blob of data
event.data.blob()
// Returns an arrayBuffer
event.data.arrayBuffer()
For most applications, json() or text() will cover what you need, depending on the format your server sends.
The critical role of waitUntil()
That basic listener is missing two essential pieces: it doesn't display a notification, and it doesn't use event.waitUntil().
Service workers run on the browser's schedule, not yours. The browser decides when to wake the worker and when to terminate it. The only signal you can send to the browser that you're still doing important work is to pass a promise to event.waitUntil(). The browser will keep the service worker alive until that promise settles.
For push events specifically, there's an additional constraint: a notification must be shown before the promise passed to waitUntil() resolves. Here's a minimal example:
self.addEventListener('push', function(event) {
const promiseChain = self.registration.showNotification('Hello, World.');
event.waitUntil(promiseChain);
});
self.registration.showNotification() displays the notification to the user and returns a promise that resolves once it's been shown. In this example, that promise is stored in a variable called promiseChain and then passed to event.waitUntil(). This is verbose, but misunderstanding what belongs inside waitUntil() or accidentally breaking the promise chain is a common source of bugs.
A more realistic scenario—fetching data from a network request and tracking the event with analytics—could look like this:
self.addEventListener('push', function(event) {
const analyticsPromise = pushReceivedTracking();
const pushInfoPromise = fetch('/api/get-more-data')
.then(function(response) {
return response.json();
})
.then(function(response) {
const title = response.data.userName + ' says...';
const message = response.data.message;
return self.registration.showNotification(title, {
body: message
});
});
const promiseChain = Promise.all([
analyticsPromise,
pushInfoPromise
]);
event.waitUntil(promiseChain);
});
Here, pushReceivedTracking() returns a promise (imagine it sends a request to your analytics provider), and a network request fetches data used for the notification's title and message. Both promises are combined with Promise.all() and the resulting promise is passed to event.waitUntil(). The browser waits until both operations complete before checking that a notification was displayed and terminating the worker.
Debugging the default notification
Chrome shows a generic "This site has been updated in the background" notification when a push message arrives and the service worker's push handler does not display a notification after the waitUntil() promise settles:
The usual culprit is that developers call self.registration.showNotification() but don't return or otherwise handle the promise it produces. That can intermittently trigger the default notification. For instance, removing the return from showNotification() in the example above creates this risk:
self.addEventListener('push', function(event) {
const analyticsPromise = pushReceivedTracking();
const pushInfoPromise = fetch('/api/get-more-data')
.then(function(response) {
return response.json();
})
.then(function(response) {
const title = response.data.userName + ' says...';
const message = response.data.message;
self.registration.showNotification(title, {
body: message
});
});
const promiseChain = Promise.all([
analyticsPromise,
pushInfoPromise
]);
event.waitUntil(promiseChain);
});
It's an easy detail to overlook. If you see the default notification, inspect your promise chains and verify that everything that must complete is properly wrapped in event.waitUntil().



