Service Worker updates and error handling with React

Federico Zivolo
4 min readAug 2, 2019

--

I recently worked to improve the Service Worker my team and I use in our React application. In this article, I’ll outline my learnings from the exercise, and some solutions to the problems we faced.

Our setup

At Quid, we rely on Create React App to handle most of our products. We mostly rely on its default configuration. When we do need to diverge from the defaults, we prefer react-app-rewired and customize-cra to tweak the configuration to fit our needs.

In most cases, our customizations are limited to adding:

  • Our custom ESLint configuration;
  • The addBundleVisualizer plugin from customize-cra to help us analyze our bundle;
  • The adjustWorkbox plugin for tweaking the workbox navigateFallbackBlacklist to exclude some API endpoints which we use;

The following examples and considerations assume you are using Create React App or a compatible solution. Also, it’s assumed that you are already familiar with the basic Service Worker concepts.

What we wanted

For a long time we kept our Service Worker enabled, but with the skipWaiting option on, this was a big source of frustration for our users.
If a new update was deployed while the users were using the application, it could break.

We wanted to make the user experience better by providing an update button to let our users update when they felt ready.

Let’s get to work!

To achieve this, we removed the skipWaiting option and added some code to detect when an update was available, and consequently show the update popup.

First thing I did was to hook into the onUpdate callback to get notified when a new Service Worker version was available.

When `isUpdateAvailable` is `true` it means we want to ask the user to update our Progressive Web App!

With the above code, we can get notified when a new version of our application is available, and with the waitingServiceWorker content we are able to trigger the actual update by running:

waitingServiceWorker.postMessage({ type: 'SKIP_WAITING' });

We could rewrite this as a custom React Hook to make it easier to use it in our codebase:

With the above code, we are able to listen to service worker updates and trigger an update when the user clicks a button bound to the updateAssets function.

Let’s see it in action by creating some UI to trigger the code outlined above.

An update button will appear when a new version is available

Handling edge cases

We have one last problem to deal with. What happens if you ignore the update button and close, and then reopen, the application?

At the moment, the update button won’t be displayed ever again, leaving the user stuck with an old version of our app forever.

To address this issue, we need to update the registerValidSW function provided by Create React App in src/serviceWorker.js. Luckily we don’t need to eject, or use react-app-rewired for this!
We just need to expose an additional callback there, we’ll add the following code right after we obtain the registration object to add support for it.

Now we can update our custom Hook to register to this new callback. To save space, I’ll just share the changed code in serviceWorker.register:

Now whenever the user opens our app, and an update has been downloaded and waiting to be installed, the same update popup will be displayed.

Uncaught (in article) Error: [Object What?]

With the solutions provided above you should be able to address all your Progressive Web App update needs, but there’s one last thing (and it’s not a good one!).

One of the most dangerous peculiarity of cache-first approach in Service Workers is the possibility to ship some broken code, and not be able to have your users receive your fix because the app is so broken that even the update process can’t run properly.

You can add a lot of error handling inside your React application, especially if you add an error boundary around your main component, but is it enough?

React’s componentDidCatch doesn’t catch all the possible errors generated by your application, and will do nothing if a 3rd party dependency introduces a critical bug.

To make sure our users never end up with a broken Service Worker cache and are always able to get the most updated version of it, we implemented a global error handler to catch any possible error we may not handle.

This error handler, internally called apocalypseHandler, will unregister the service worker if it finds a newer version is available, and then reload the page so that the browser is able to start afresh. We may attempt to update the service worker rather than unregister it, but something may be broken in the update process itself, so we don’t want to rely on any code external to our error handler.

We want to execute the code below as soon as possible, before we even attempt to render our application or run any other code. This minimizes the chances of our event handler failing to register due to prior errors.

Make sure to put this on top of your index.js!

That’s all folks!

By handling background updates and making sure we don’t saddle our users with a broken version of the app, we’ve made our app a delight to use — both for our users and our developers.

If you have any other suggestions to improve this setup, please let me know in the comments!

Special thanks to Kumar Harsh which helped me by proof reading this article.

--

--

Federico Zivolo

JavaScript Engineer and UI Specialist. Creator of Bootstrap Material Design and Popper.js. React developer. OpenSource addicted.