Async functions in Service Workers

Use async / await in ServiceWorker code because browsers support both.

If you want to make a simple page like the one below work without connection, you have an exciting browser feature - Service Worker.

1
2
3
4
5
6
7
<head>
<link rel="stylesheet" href="app.css">
</head>
<body>
<h1>My page</h1>
<p>A lot of content here</p>
</body>

Not every browser supports service workers yet, as we can see from http://caniuse.com/#feat=serviceworkers

ServiceWorker browser support

We can test if the service worker is supported before loading its code by checking the navigator object

1
2
3
4
5
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js')
}
</script>

Inside the service worker script sw.js we can cache all resources when the worker is executed for the first time (the install event). Then we can intercept fetch events from the page (which include both Ajax and static requests) and serve the cached versions. If the server goes down, or WiFi drops, the page continues to work by serving the originally cached resources.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
self.addEventListener('install', event => {
const urlsToCache = ['/', 'app.css']
event.waitUntil(
caches.open('demo')
.then(cache => cache.addAll(urlsToCache))
)
})

self.addEventListener('fetch', event => {
const url = event.request.url
event.respondWith(
caches.open('demo')
.then(cache => cache.match(event.request))
)
})

Notice this code does not even include error handling, yet it is a little hard to read. Mostly because it combines event callback with a promise chain in both cases. For example the fetch event waits on the promise we return while searching for the matching cached response

1
2
3
4
event.respondWith(
caches.open('demo')
.then(cache => cache.match(event.request))
)

Imagine if we wanted to fetch a resource if it was not found in the cache, the promise chain would keep growing. The code does not look as bad as a callback pyramid of Doom, yet is still complicated and hard to read.

Well, some people argue that async / await functions are better than promises. Maybe yes maybe no, but the important thing about async functions is that they return a Promise!

1
2
3
function async foo() { return 'bar' }
foo()
.then(console.log) // "bar"

Great, how does it help us inside the service worker script? If you look at the browser support for async functions, it covers all browsers that support service worker and more.

Async functions browser support

caution Samsung mobile browser that supports service workers does NOT support async functions yet, thus the code below will not work. I suggest transpiling the sw.js and serving the ES5 version based on user agent header.

The wide browser support means we can use the async functions to code our service worker and it should just work. The same code as above looks much cleaner if we separate caching logic into their own functions. The service worker life cycle event handlers become just tiny glue functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const cacheResources = async () => {
const urlsToCache = ['/', 'app.css']
const cache = await caches.open('demo')
return cache.addAll(urlsToCache)
}

self.addEventListener('install', event =>
event.waitUntil(cacheResources())
)

const cachedResource = async req => {
const cache = await caches.open('demo')
return await cache.match(req)
}

self.addEventListener('fetch', event =>
event.respondWith(cachedResource(event.request))
)

Perfecto!