
To check if the user is online in JavaScript, read navigator.onLine and listen for the online and offline events on window. But true only means the device has a network, not that the internet works. To be sure, send a small fetch() to your own server with a timeout and treat any failure as offline.
In this post we look at what navigator.onLine really tells you, react to the online and offline events, write a real connection check with fetch and AbortSignal.timeout(), build an offline banner, and do the same in Node.js. The quick answer (it uses await at the top level, so run it in the browser console or in a <script type="module">):
const online = await fetch('/favicon.ico?t=' + Date.now(), {
method: 'HEAD', cache: 'no-store', signal: AbortSignal.timeout(5000),
}).then((res) => res.ok, () => false);
Key takeaways
navigator.onLine === falsemeans the browser thinks there's no network at all.trueonly means it might be online: a Wi-Fi router with no internet still counts.- Don't poll
navigator.onLinewithsetInterval. The browser firesofflineandonlineonwindowwhen it changes. - The only real test is a request. Send a
HEADrequest to a small file on your own server, withcache: 'no-store'and a timestamp in the URL so no cached copy answers. - Always add a timeout:
AbortSignal.timeout(5000). Without one, a request to a network that has stopped answering can wait a long time. - Don't ping google.com from a web page. CORS blocks the response, so the check fails even when the internet is fine.
- Node.js has no
navigator.onLine(it'sundefined). Use the samefetchwith a timeout there.
Our 2021 video builds the first version: a page that reads navigator.onLine every second and prints Online or Offline. This post keeps that idea, swaps the timer for events and adds the check that the 2021 version was missing:
What does navigator.onLine actually mean?
navigator.onLine is a true or false that the browser keeps up to date. It answers a smaller question than most people think. The HTML standard, which browsers follow, puts it like this:
"Returns false if the user agent is definitely offline (disconnected from the network). Returns true if the user agent might be online."
So false should mean offline, and true is only a maybe. The same section of the standard adds: "This attribute is inherently unreliable. A computer can be connected to a network without having Internet access." MDN gives the usual example: "In general, connection to LAN is considered online, even though the LAN may not have Internet access." A LAN is the local network in your home or office: your laptop, your phone and the Wi-Fi router. Even a virtual one can count: MDN's example is virtual machine software whose virtual network adapters are always "connected".
That gives three cases, and only the request check gets all three right:

| Case | onLine says | Real check says |
|---|---|---|
| Wi-Fi off | false | false |
| Wi-Fi on, but the router has no internet | true | false |
| Wi-Fi on and the internet works | true | true |
It can be wrong the other way too. MDN notes that "On Windows, the online status is determined by whether it can reach a Microsoft home server, which may be blocked by firewalls or VPNs, even if the computer has Internet access." Its advice follows from that: "you should not disable features based on the online status, only provide hints when the user may seem offline." So use navigator.onLine to show a message, never to switch your app off.
The good news: it works everywhere. MDN marks both events "Baseline Widely available", and all three have worked in every major browser since July 2015.
How do I detect when the user goes offline or comes back online?
Listen for the offline and online events on window. The browser fires one each time navigator.onLine changes, so you don't need a timer. Save this as status.js and load it in any page:
function showStatus() {
console.log(navigator.onLine ? 'online' : 'offline');
}
window.addEventListener('online', showStatus);
window.addEventListener('offline', showStatus);
showStatus();
Open the page and it logs online. Now open DevTools, go to the Network tab and pick Offline from the throttling menu. The console logs offline. Switch back and it logs online again. Turning your Wi-Fi off does the same.
The last line, showStatus(), matters: the events only fire on a change, so you also need to read the value once when the page loads.
Our 2021 version ran a check every second with setInterval. That works, but it does 86,400 checks a day to catch a change the browser would have told it about anyway. MDN also warns about the events: the online event "shouldn't be used to determine the availability of a particular website. Network problems or firewalls might still prevent the website from being reached." For that, you need a real request.
How do I check if the internet is really working?
Ask a server for something small and see if an answer comes back in time. Here's the quick answer from the top of the post as a function you can reuse, in is-online.js:
async function isOnline(url = '/favicon.ico', ms = 5000) {
try {
const res = await fetch(`${url}?t=${Date.now()}`, {
method: 'HEAD',
cache: 'no-store',
signal: AbortSignal.timeout(ms),
});
return res.ok;
} catch {
return false;
}
}
isOnline().then((online) => console.log(online ? 'online' : 'offline'));
Each part has a job:
'/favicon.ico'is a small file that most websites already have, on your own server. Any small file works, and a/pingroute that just answers 204 is even better. Pass a URL with no?in it, because the function adds its own.method: 'HEAD'asks for the headers only, so no file comes down. If your server answers HEAD with 405 (not allowed), use'GET'on a tiny file instead.cache: 'no-store'and?t=${Date.now()}stop a stored copy from answering. MDN saysno-storemeans "The browser fetches the resource from the remote server without first looking in the cache". The timestamp gives every check a new URL, so a cache along the way (a proxy, or a service worker that caches by URL) has nothing stored to answer with. Leave both out and the cache can answer: in our test, a cached/favicon.icostill came back with status 200 after we had stopped the server.AbortSignal.timeout(ms)gives up after 5 seconds. It then rejects with aTimeoutError, thecatchcatches it, and we say offline.res.okistrueonly for a 2xx status: MDN says it states "whether the response was successful (status in the range 200-299) or not." MDN: "A fetch() promise does not reject if the server responds with HTTP status codes that indicate errors (404, 504, etc.)." A 404 or 503 means a server did answer, so the internet works, but your app still can't get what it needs, so we count it as offline too. If you want to tell the two apart, returnres.statusand show a different message for each.
Every way this can fail ends up as false:

We ran isOnline() against a test server for each case. A 200 gave true. A 404, a 503, a stopped server and a server that never answered all gave false, the last one after the 5 seconds were up. In Chrome with the network set to Offline it gave false at once.
navigator.onLine first and skip the request when it's false? Because of the Windows case above, where it says false while the internet works. When the device really is offline, the request fails straight away anyway, so the check costs nothing.Without AbortSignal.timeout()
AbortSignal.timeout() is newer than the rest: MDN marks it "Baseline 2024", working in the latest browsers since April 2024. For older browsers, use an AbortController and your own timer. Save this as is-online-old.js:
async function isOnline(url = '/favicon.ico', ms = 5000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
try {
const res = await fetch(`${url}?t=${Date.now()}`, {
method: 'HEAD',
cache: 'no-store',
signal: controller.signal,
});
return res.ok;
} catch {
return false;
} finally {
clearTimeout(timer);
}
}
It works the same, with two small differences. A timeout now rejects with an AbortError instead of a TimeoutError. And finally clears the timer as soon as the answer arrives, which MDN recommends when you "want to definitively cancel timeouts early".
Why not ping google.com?
It's the first idea everyone has: Google is always up, so if fetch('https://www.google.com') works, the internet works. From a web page, it doesn't. MDN explains why: a page using fetch() "can only request resources from the same origin the application was loaded from unless the response from other origins includes the right CORS headers." Google's home page doesn't send those headers to your site, so the browser blocks the answer and fetch rejects with a TypeError (in Chrome the message is Failed to fetch). Your check would say offline every time.
An origin is the scheme, host and port together, like https://blog.letsbug.in. MDN: "Two objects have the same origin only when the scheme, hostname, and port all match." We tried it with two test servers on different ports, which counts as a different origin. Chrome's console showed "has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource." Adding mode: 'no-cors' stops the error, but the answer comes back "opaque": status 0 and ok false, so you still can't read it. The promise does resolve, so you know something came back, but you can't tell a 200 from a 500. Our isOnline() would say offline because ok is false, and the check would still depend on someone else's server.
Even if it worked, your own server is the better choice. It's the one your app actually needs, and the check doesn't hand your users' IP addresses to another company or break on networks that block that site.
Show an offline banner
Now let's put the pieces together: a page that shows a message while the connection is down and hides it when it comes back. Save this as banner.html and serve it from your site (opened as a file, it has no server to check, so it always says offline):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Online status</title>
<style>
#net-status { position: fixed; left: 0; right: 0; bottom: 0; margin: 0;
padding: 12px; background: #b3261e; color: #fff; text-align: center; }
#net-status:empty { padding: 0; }
</style>
</head>
<body>
<h1>My app</h1>
<p id="net-status" role="status"></p>
<script>
async function isOnline(url = '/favicon.ico', ms = 5000) {
try {
const res = await fetch(`${url}?t=${Date.now()}`, {
method: 'HEAD',
cache: 'no-store',
signal: AbortSignal.timeout(ms),
});
return res.ok;
} catch {
return false;
}
}
const statusBox = document.getElementById('net-status');
async function update() {
const online = await isOnline();
statusBox.textContent = online ? '' : "You're offline. We'll keep trying.";
}
window.addEventListener('online', update);
window.addEventListener('offline', update);
setInterval(update, 30000);
update();
</script>
</body>
</html>
- The events make it react at once when the Wi-Fi drops or comes back.
setInterval(update, 30000)covers the case the events miss: the Wi-Fi stays connected but the internet behind it stops. One tiny HEAD request every 30 seconds is cheap. Your app's own failed requests are a good moment to callupdate()too.- The message lives in a
role="status"element that's always on the page, and only its text changes. MDN describesstatusas "a live region containing advisory information", so screen readers read the new text out without moving the user's focus. The:emptyrule shrinks the red bar to nothing when there's no text. It doesn't usedisplay: none, so the status element stays on the page for screen readers when the first message arrives. textContentinstead of the 2021 version'sinnerHTML: it's plain text, so there's nothing to parse as HTML.
We loaded this page from a local server in Chrome and switched the network to Offline and back. The bar showed "You're offline. We'll keep trying." straight away and disappeared when the network came back. With the network still "on" but the server stopped, the bar appeared at the next check.
How do I check the internet connection in Node.js?
Node.js has a navigator object since version 21, but it has no onLine. In Node.js 24, navigator.onLine is undefined, so code that copies the browser version decides you're always offline. There's no CORS in Node.js either, so you can check any server you like. The same fetch works, because Node.js has a built-in fetch, no longer experimental since version 21. Save this as check-internet.mjs:
async function canReach(url, ms = 5000) {
try {
const res = await fetch(url, { method: 'HEAD', signal: AbortSignal.timeout(ms) });
return res.ok;
} catch {
return false;
}
}
console.log(await canReach('https://example.com/'));
$ node check-internet.mjs
true
With the network unplugged, fetch fails with TypeError: fetch failed, and canReach returns false. Point it at the API your program really needs, rather than example.com, and use a URL that answers 2xx without a login: a 401 counts as offline here.
You'll also see dns.lookup() suggested for this. It isn't a good test. The Node.js docs say it "uses the operating system facilities to perform name resolution. It may not need to perform any network communication." So it can succeed with no internet, from your computer's own list of names: dns.lookup('localhost') works with no network at all. And when it fails, the docs warn that the error code ENOTFOUND appears "not only when the host name does not exist but also when the lookup fails in other ways". A real request is the only answer you can trust.
Common mistakes
Trusting navigator.onLine === true
It only means "has a network". A router with no internet, a LAN, or a virtual network adapter all count as online. Use it for a quick hint, then confirm with a request.
Checking on a timer instead of using the events
Reading navigator.onLine every second, as our 2021 version did, finds nothing the online and offline events don't already tell you. If you poll at all, poll the request check, and slowly.
A fetch with no timeout
When the network stops answering without failing (a weak signal, a server that never replies), a plain fetch can stay pending for a long time, and your banner never appears. In our test in Chrome, a request to a server that never answered was still waiting after 30 seconds. Always pass a signal.
Getting a cached answer
Without cache: 'no-store' and the ?t= timestamp, the browser can answer from its cache, and the check says "online" when nothing left the device.
Treating "fetch didn't throw" as "online"
A 404 or 503 doesn't make fetch reject. Check res.ok, and point the check at a file that really exists.
Switching features off when navigator.onLine is false
On Windows it can be false while the internet works. Show a message and let the user keep going: if a request then succeeds, they weren't offline.
Work these out first, then try them in the browser console.
1) Your laptop is on the home Wi-Fi, but the router's internet cable is unplugged. What does navigator.onLine say, and what does isOnline() return?
2) Your site has no favicon, so HEAD /favicon.ico gets a 404. What does isOnline() return, and why?
3) Our 2021 page checks navigator.onLine every second. In which of the three cases in the picture above does it show the wrong message?
Show the answers
1) navigator.onLine is true: the laptop is connected to a network. isOnline() returns false: the request can't reach your server, so it fails or times out after 5 seconds.
2) false. fetch doesn't reject on a 404, but res.ok is false for anything outside 200 to 299. Point the check at a file that exists, or add a /ping route.
3) The middle one: connected to Wi-Fi but no internet. It says "Online | Connected to Internet" because navigator.onLine is true.
Questions people ask
How do I check if the user is online in JavaScript?
Read navigator.onLine for a quick answer and listen for the online and offline events on window to hear about changes. To be sure the internet works, send a HEAD request to your own server with cache: 'no-store' and AbortSignal.timeout(5000), and treat an error, a timeout or a status outside 200 to 299 as offline.
Is navigator.onLine reliable?
Not on its own. The HTML standard calls it "inherently unreliable". true only means the device is connected to a network, which may have no internet. On Windows it can even be false while the internet works, when a firewall or VPN blocks Microsoft's check server. Use it as a hint and confirm with a request.
What's the difference between navigator.onLine and the online event?
navigator.onLine is the current value. The online and offline events fire on window when that value changes. Read the value once when the page loads, then let the events tell you about changes.
How do I check the internet connection without a CORS error?
Send the request to your own site's origin, for example /favicon.ico or a /ping route. Requests to the same origin never need CORS headers. Requests to another site, like google.com, are blocked unless that site sends an Access-Control-Allow-Origin header for you.
How do I check the internet connection in Node.js?
Use the built-in fetch with a timeout: await fetch(url, { method: 'HEAD', signal: AbortSignal.timeout(5000) }), and treat an error or a status outside 200 to 299 as offline. Node.js has no navigator.onLine, and dns.lookup() can succeed without any network.
How do I test offline mode without turning off my Wi-Fi?
In Chrome or Edge DevTools, open the Network tab and choose Offline from the throttling menu. navigator.onLine becomes false, the offline event fires, and every fetch fails.
Keep going
- What actually happens on the way to the server your check is asking: how the internet works, step by step.
- Give your API a
/pingroute to check: a REST API in Express.js. - Your front end and API on different servers? Deploy a Vite React app to Vercel and an Express API to Render, CORS included.
- Watch the original video, how to check network status in JavaScript, then the rest of the letsBug Code videos on YouTube.
Checked on 22 October 2026: every code sample in this post was run in Node.js 24.20.0 and in Chrome 155 (with its network switched off and on), and every fact was checked against the sources below.
Sources
- HTML Living Standard: System state and capabilities (
navigator.onLine, theonlineandofflineevents) - MDN: Navigator: onLine property, Window: online event and Window: offline event
- MDN: Window: fetch() method and Request: cache property (
no-store) and Response: ok property - MDN: AbortSignal: timeout() static method and AbortController
- MDN: Cross-Origin Resource Sharing (CORS), Origin and ARIA: status role
- Node.js documentation: Globals (
fetch,navigator) and DNS (dns.lookup())