How to really know if your webapp is online

If you search for "check online JavaScript", you'll quickly come across the navigator.onLine property. Unsurprisingly, it returns true if you're online, and false if you aren't. In addition, the window.ononline and window.onoffline events notify you whenever that value changes.

False positives

If it was quite this easy, I wouldn't be sitting here writing a blog post about it. In fact, if we look at the spec, we find the following:

The onLine attribute must return false if the user agent will not contact the network when the user follows links or when a script requests a remote page (or knows that such an attempt would fail), and must return true otherwise.

In other words, navigator.onLine being true does not necessarily mean that there is a working internet connection. Most implementations seem to only be checking if there's a connected network adapter. There are a number of circumstances where that's the case, but the internet is still unreachable. For example:

Just try it

How do you actually know if there is a working internet connection? Well, there's really only one way. You have to try it out. Do a fetch request to an appropriate target and see whether or not you get a network error. A simple example is below:

async function isInternetConnectionWorking() {
    if (!navigator.onLine) {
        return false;
    }
    const headers = new Headers();
    headers.append('cache-control', 'no-cache');
    headers.append('pragma', 'no-cache');
    try {
        await fetch(window.location.origin, { method: 'HEAD', headers });
        return true;
    } catch (error) {
        if (error instanceof TypeError) {
            return false;
        }
        throw error;
    }
}

A few things to note:

Actually, don't even try

While the above code works, it has a subtle problem: Potentially every time the function isInternetConnectionWorking() is called, it sends a new, non-cached HEAD request. That might put an undue burden on the network.

A typical web app already makes plenty of requests during normal use. We can monitor those to detect whether or not we're online. Only if no request has occurred recently, we send one like in the above example to check if we're still connected.

The revised example below uses a service worker to intercept network requests and update the online status. It also adds a special route, /is-online, that we can fetch to get the current online status. Note that service workers only work in modern browsers, and only on secure pages (served via HTTPS). If you're still not using HTTPS, well, you should stop reading this blog and get on it.

index.js

navigator.serviceWorker.register('/sw.js');
async function isInternetConnectionWorking() {
    if (!navigator.onLine) {
        return false;
    }
    const response = await fetch('/is-online');
    if (response.status === 200) {
        const onlineStatus = response.json();
        return onlineStatus.value;
    }
    return true;
}

sw.js

// Interval in ms before we re-check the connectivity - change to your liking
const ONLINE_TIMEOUT = 10000;
let onlineStatus = { value: true, timestamp: new Date().getTime() };

async function getFromCache(request) {
    // get from cache, if you have one...
    throw new TypeError();
}

async function tryGetFromNetwork(request) {
    const timestamp = new Date().getTime();
    try {
        const response = await fetch(request);
        onlineStatus = { value: true, timestamp };
        return response;
    } catch (error) {
        if (error instanceof TypeError) {
            onlineStatus = { value: false, timestamp };
        }
        throw error;
    }
}

async function getOnlineState() {
    const now = new Date().getTime();
    const headers = new Headers();
    headers.append('cache-control', 'no-store');

    // If the last online status is recent, return it
    if (now - onlineStatus.timestamp < ONLINE_TIMEOUT) {
        return new Response(
            JSON.stringify(onlineStatus),
            { status: 200, statusText: 'OK', headers }
        );
    }
    // Otherwise, attempt a real fetch to re-check the connection
    else {
        try {
            await fetch(location.origin, { method: 'HEAD', headers });
            onlineStatus = { value: true, timestamp: now };
        } catch (error) {
            if (error instanceof TypeError) {
                onlineStatus = { value: false, timestamp: now };
            } else {
                throw error;
            }
        }
    }
    // Recursive call, this time the new status will be returned
    return await getOnlineState();
}

self.addEventListener('fetch', event => {
    if (event.request.method === 'GET' && event.request.url === `${location.origin}/is-online`) {
        event.respondWith(getOnlineState());
    }
    else {
        event.respondWith(
            tryGetFromNetwork(event.request)
                // If the fetch fails, get a response from cache if you have one
                // If not, just delete the next line
                .catch(() => getFromCache(event.request))
        );
    }
});

That's it! Now, every time your app makes a request, the service worker will note whether it succeeded or not. From your app, you can fetch /is-online to get the latest connection status. If it's older than ONLINE_TIMEOUT milliseconds, the service worker will re-check for you.