Back in the early days of HTML5, navigator.onLine
was introduced as a simple way to ask: is the browser online or offline? In theory, it’s a boolean reflecting whether the user agent is connected to a network. Over time, it became a convenient hook: you can listen to window.addEventListener('online', …)
and offline
events to detect when connectivity changes.
However, as browser vendors and operating systems evolved, the implementation of navigator.onLine
diverged in subtle but crucial ways. Some platforms interpret “online” merely as “network interface is up” (e.g. you have a LAN or virtual adapter) even if there is no path to the internet. Others test connectivity by pinging a known host (e.g. Windows sometimes probes Microsoft servers) — but such probes may be blocked by firewalls, proxies, or VPNs.
Thus, navigator.onLine === true
does not guarantee that your app can reach your server or any desired endpoint. Conversely, navigator.onLine === false
is a stronger signal that the machine isn’t even network-connected (though corner cases may exist). In practice, using navigator.onLine
alone leads to false positives (indicating “online” though APIs fail) and thus degraded UX.
To work around this, engineers have adopted a hybrid approach: use navigator.onLine
as a cheap early check, then perform a lightweight “reachability probe” (e.g. via fetch
) to confirm that your backend or a known resource is reachable.
Below I propose a modern, resilient strategy, then walk through sample implementations.
Strategy: hybrid reachability + fallback, with timeouts
Here’s a conceptual flow for determining “actually online” (i.e. your app can meaningfully communicate with its services):
flowchart TD A{navigator.onLine?} -->|false| B[Declare offline] A -->|true| C[Perform reachability probe] C -->|success| D[Declare online] C -->|failure| E[Declare offline]

Key design principles:
- Fail fast with timeout: If a probe takes too long (e.g. > 1 s), treat it as unreachable rather than waiting indefinitely.
- Use a cheap HTTP request: Use
HEAD
or a static “ping” endpoint rather than fetching large payloads. - Throttle checks: Don’t probe too frequently (every second) in normal operation; back off after failures.
- Fallback modes: If reachability fails repeatedly, optionally allow degraded mode (read-only, cached responses) rather than simply showing “offline”.
- Listen to browser events: When the browser fires
online
oroffline
events, trigger a fresh check.
This approach gives you a more trustworthy “connected or not” indicator before making real API calls.
Sample implementation in plain JavaScript
Here’s a standalone utility you can drop into your app:
async function canReach(url, timeout = 1000) {
// Quick check: if browser thinks it’s offline, bail early
if (!navigator.onLine) {
return false;
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
// Use HEAD so we don’t download body payload
const resp = await fetch(url, {
method: 'HEAD',
signal: controller.signal,
cache: 'no-cache',
});
return resp.ok;
} catch (err) {
return false;
} finally {
clearTimeout(timer);
}
}
// A wrapper that caches last result and avoids over-probing
class OnlineChecker {
constructor(pingUrl = '/favicon.ico', timeout = 1000, probeInterval = 30000) {
this.pingUrl = pingUrl;
this.timeout = timeout;
this.probeInterval = probeInterval;
this.lastResult = null;
this.probeTimer = null;
window.addEventListener('online', () => this.triggerProbe());
window.addEventListener('offline', () => {
this.lastResult = false;
this.clearProbeTimer();
});
}
async triggerProbe() {
const ok = await canReach(this.pingUrl, this.timeout);
this.lastResult = ok;
// schedule next
this.clearProbeTimer();
this.probeTimer = setTimeout(() => this.triggerProbe(), this.probeInterval);
}
clearProbeTimer() {
if (this.probeTimer) {
clearTimeout(this.probeTimer);
this.probeTimer = null;
}
}
start() {
this.triggerProbe();
}
stop() {
this.clearProbeTimer();
}
isOnline() {
return this.lastResult;
}
}
// Usage:
const checker = new OnlineChecker('/ping', 800, 15000);
checker.start();
// Later, e.g. before making API call:
if (checker.isOnline()) {
// proceed
} else {
// show offline UI or fallback
}
Notes:
- I used
/ping
(you could use any lightweight endpoint your server supports). - The
OnlineChecker
listens for browseronline
/offline
events so it re-checks when connectivity changes. - The
probeInterval
ensures you don’t hammer your server continuously. - You can adapt or extend this to exponential backoff (e.g. double the interval after failures) or to stop probing after a threshold.
React example (hooks + context)
Below is a React-centric version using hooks and context, so you can access isOnline
throughout your component tree.
import React, { createContext, useContext, useEffect, useState } from 'react';
async function pingUrl(url, timeout = 1000) {
if (!navigator.onLine) return false;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
const resp = await fetch(url, { method: 'HEAD', signal: controller.signal, cache: 'no-cache' });
return resp.ok;
} catch {
return false;
} finally {
clearTimeout(timer);
}
}
const OnlineContext = createContext({ isOnline: true });
export function OnlineProvider({ ping = '/ping', timeout = 1000, intervalMs = 15000, children }) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
let intervalId = null;
const check = async () => {
const ok = await pingUrl(ping, timeout);
setIsOnline(ok);
};
check();
intervalId = setInterval(check, intervalMs);
window.addEventListener('online', check);
window.addEventListener('offline', () => setIsOnline(false));
return () => {
if (intervalId) clearInterval(intervalId);
window.removeEventListener('online', check);
window.removeEventListener('offline', () => setIsOnline(false));
};
}, [ping, timeout, intervalMs]);
return <OnlineContext.Provider value={{ isOnline }}>{children}</OnlineContext.Provider>;
}
// Hook to consume
export function useOnline() {
return useContext(OnlineContext).isOnline;
}
Then in your app:
function App() {
const isOnline = useOnline();
if (isOnline === null) {
return <div>Checking connectivity…</div>;
}
if (!isOnline) {
return <div>No internet connection.</div>;
}
return <div>Welcome! (You are online.)</div>;
}
This pattern ensures React components re-render when reachability changes, and you get a global “online state.”
Caveats, trade-offs, and best practices
- Some corporate networks block
HEAD
or static ping endpoints, or intercept them (e.g. captive portals). Pick a ping URL that’s likely to bypass such restrictions (e.g. one you control). - Don’t trust continuous “failure” = offline forever. Try periodic retries (e.g. backoff logic).
- If your app must always work offline-first, consider service workers + cached responses so some core features operate without connectivity.
- On mobile, connectivity may fluctuate rapidly; use heuristics (e.g. require 2 successive failures) before declaring offline.
- Keep the latency of your reachability probe low. A 1 s timeout is reasonable, though network conditions may vary.
Summary & next steps
You now understand why navigator.onLine === true
is too weak a signal: it may merely reflect that a network interface is up, not that true connectivity exists. The hybrid method I presented (navigator hint + reachability probe + timeout + event listeners) offers a more robust indicator.
Next, you might:
- Adapt the code to TypeScript for stronger typing.
- Add exponential backoff or “circuit breaker” logic after repeated failures.
- Integrate with your API client so all network calls pass through a gate guarded by connectivity.
- Add logging/telemetry for diagnostics (e.g. how often probes fail in real users).