Handling broken images with the service worker

A few years ago, I wrote about how we can use css to style broken images. The technique leveraged on the fact that any styling to the ::before or ::after pseudo-elements on the <img> element will only be applied if the image doesn’t load. So, we could style those elements and they would only display if the image was broken.

Here’s an example of how I’ve styled broken images on this site:

Broken image

There are pros and cons to handling broken images this way. One limitation is browser support, as this technique doesn’t work in some major browsers like Safari.

Having recently done a lot of work with service workers, it occurred to me that we could use the service worker to handle broken images in a different way. Since the service worker can tell if an image file isn’t able to be fetched, we can handle that condition by, for example, serving a different image to the browser.

Intercepting requests for broken images

In the service worker fetch event, we can tell if and when a request the browser makes goes wrong, whether that be because the user is offline or because the response to the fetch request was bad.

    self.addEventListener('fetch', (e) => {
        e.respondWith(
            fetch(e.request)
                .then((response) => {
                    if (response.ok) return response;

                    // User is online, but response was not ok

                })
                .catch((err) => {

                    // User is probably offline

                })
        )
    });

In either of these scenarios, we can check to see if the failed request was for an image and do whatever we like in response.

    function isImage(fetchRequest) {
        return fetchRequest.method === "GET" 
               && fetchRequest.destination === "image";
    }

    self.addEventListener('fetch', (e) => {
        e.respondWith(
            fetch(e.request)
                .then((response) => {
                    if (response.ok) return response;

                    // User is online, but response was not ok
                    if (isImage(e.request)) {
                        // do something
                    }

                })
                .catch((err) => {

                    // User is probably offline
                    if (isImage(e.request)) {
                        // do something
                    }

                })
        )
    });

Serving a “broken image” image

One way to handle requests for images that don’t resolve would be to send a placeholder image in its place. For example, we can have an image like the one below to show the user that the image is broken.

Broken image icon with text below that says 'image nto found'

We can implement this by responding to the fetch request with a new request for the palceholder file, /broken.png.

    self.addEventListener('fetch', (e) => {
        e.respondWith(
            fetch(e.request)
                .then((response) => {
                    if (response.ok) return response;

                    // User is online, but response was not ok
                    if (isImage(e.request)) {
                        // Fetch the broken image placeholder instead
                        return fetch("/broken.png");
                    }

                })
                .catch((err) => {

                    // User is probably offline
                    if (isImage(e.request)) {
                        // do something
                    }

                }) // end fetch
        )
    });

This will work when the user is online, but if we want this to also work offline, we will need to cache the placeholder image. This is typically done during the install phase of the service worker lifecycle.

    self.addEventListener('install', (e) => {
        self.skipWaiting();
        e.waitUntil(
            caches.open("precache").then((cache) => {

                // Add /broken.png to "precache"
                cache.add("/broken.png");

            })
        );
    });

Once the image is in the cache, we can respond to the fetch request with a response from the cache instead of having to make a new request over the network.

    self.addEventListener('fetch', (e) => {
        e.respondWith(
            fetch(e.request)
                .then((response) => {
                    if (response.ok) return response;

                    // User is online, but response was not ok
                    if (isImage(e.request)) {
                        // Get broken image placeholder from cache
                        return caches.match("/broken.png");
                    }

                })
                .catch((err) => {

                    // User is probably offline
                    if (isImage(e.request)) {
                        // Get broken image placeholder from cache
                        return caches.match("/broken.png");
                    }

                })
        )
    });

I created a GitHub Gist with the full implementation in case you want to use it in your own projects.