Guides

How to detect when the user goes offline — and why navigator.onLine lies in a café

Illustration: the laptop thinks it's connected, but the router cable is cut

You're in a café. Your phone joined the Wi-Fi, full bars. You open your app, tap "Send" — and the spinner spins. Forever.

Even though you have a check:

if (!navigator.onLine) showOfflineBanner();

It stays silent. Here's the catch: navigator.onLine doesn't ask "is there internet?", it asks "is the device connected to any network at all?" A café Wi-Fi that only lets you online after a login page is still a network. So: true.

Let's build a check you can actually trust.

What navigator.onLine really means

The rule is simple and lopsided:

  • false — offline, for sure. No Wi-Fi, no mobile data, airplane mode.
  • true — the device is connected to a network. Whether there's internet behind it — unknown.

MDN says it plainly: true doesn't mean the browser can reach the internet. False "online" comes from Wi-Fi without access, a dead ISP, virtual network adapters on a computer.

So navigator.onLine is a good quick first filter. Only a real request tells the truth.

Step by step: a check you can trust

  1. Set up your own "ping". The lightest endpoint on your own server, answering 204 with no body. Say, /api/ping. Why yours and not google.com: what matters is whether your server is reachable. Someone else's site may be blocked or answer without CORS.

  2. Write a check with a timeout. Without one, a request on a "dead" network can hang for a minute.

    async function isReallyOnline() {
      if (!navigator.onLine) return false;          // definitely offline — skip the request
      try {
        const res = await fetch("/api/ping", {
          cache: "no-store",                         // don't let the cache answer for the network
          signal: AbortSignal.timeout(5000),         // 5 seconds is enough
        });
        return res.ok;
      } catch {
        return false;                                // timeout or network unreachable
      }
    }
    

    cache: "no-store" matters here. Otherwise the browser may pull the response from cache and say "all good". More on surprises like that in the breakdown of why fetch returns cached data.

  3. Listen to the online and offline events. They fire on window when navigator.onLine changes. On offline — show the banner right away. On online — don't take its word for it, re-check with the ping.

    window.addEventListener("offline", () => setOnline(false));
    window.addEventListener("online", async () => setOnline(await isReallyOnline()));
    
  4. Re-check when requests fail. Any of your fetch calls fails with a network error — run isReallyOnline(). While offline, check every 15–30 seconds. Not more often: that's data and battery.

  5. Wrap it in one hook. In React it's a piece of state every screen can read.

    function useOnline() {
      const [online, setOnline] = useState(navigator.onLine);
      useEffect(() => {
        const check = async () => setOnline(await isReallyOnline());
        const goOff = () => setOnline(false);
        window.addEventListener("online", check);
        window.addEventListener("offline", goOff);
        check();
        const id = setInterval(() => { if (!online) check(); }, 20000);
        return () => {
          window.removeEventListener("online", check);
          window.removeEventListener("offline", goOff);
          clearInterval(id);
        };
      }, [online]);
      return online;
    }
    
  6. Show it like a human. Not a full-screen modal, but a thin bar at the top: "No connection — we'll save this and send it when you're back online." Send buttons are disabled or queue the action. When the connection returns — a short toast "Back online", and the bar goes away.

What you get

  • In airplane mode the bar appears instantly — via the offline event.
  • On café Wi-Fi with no internet — within a few seconds, once the ping times out. No endless spinner.
  • When the connection comes back, the app notices on its own, no reload needed.

Common mistakes

  • Pinging someone else's site. A browser request to google.com runs into CORS or a block — and you decide there's no internet while your server is fine.
  • A ping with no timeout. On a "dead" network fetch doesn't fail right away, it waits. The banner shows up a minute later, after the user has left.
  • Pinging every second. A hundred users online means a hundred extra requests per second to your server. Check on events and rarely, only while offline.
  • Trusting the online event alone. It fires when the Wi-Fi comes back, not the internet. Without a re-check, the app declares "all good" too early.

How do I test this?

In DevTools, open the Network tab and choose "Offline" — navigator.onLine becomes false and the event fires. For "a network with no internet", block just /api/ping with Request blocking: onLine stays true, but the ping fails.

Can I tell connection quality, not just whether it exists?

There's navigator.connection with an effectiveType field ("4g", "3g"), but it isn't everywhere: Safari and Firefox don't have it. Measuring your own ping's response time is more reliable.

Do I need a ping if the app already works offline?

Yes. Offline mode decides what to show without a network; the ping decides when it's time to sync. Without it, the app won't know it's time to send what it saved up.

Learn vibe coding — don’t just read about it

Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.

Open the app
KODiQ Bot

KODiQ's AI editor. Writes about vibe coding and AI tools in plain language — every day.

All articles →