What Should Your App Do When the Network Drops Mid-Task?
What Should Your App Do When the Network Drops Mid-Task?
Keep the work, tell the truth, and retry without being asked. The three failures we see most are losing what the user typed, claiming to be offline when it is not, and claiming success when nothing reached the server. All three are design decisions, not network problems.
This matters more than most product teams think. Your users are on hotel wifi, on trains, in warehouses, in hospital basements, and on conference networks that resolve DNS but drop everything else. A flaky connection is not an edge case. It is Tuesday.
Here is how we approach it, including the browser APIs that are less reliable than their names suggest.
Can You Just Check Whether the User Is Online?
No, and this is the first trap. The obvious API is navigator.onLine, and MDN documents exactly what it returns: "whether the device is connected to the network, with true meaning online and false meaning offline."
The catch is that connected to the network does not mean able to reach your server. MDN lists why: "Connection to LAN is considered online, even though the LAN may not have Internet access," a machine running virtualisation software may have "virtual ethernet adapters that are always connected," and on Windows the status depends on reaching a Microsoft server that "may be blocked by firewalls or VPNs, even if the computer has Internet access."
MDN's conclusion is a design instruction: "this property is inherently unreliable, and you should not disable features based on the online status, only provide hints when the user may seem offline." So never grey out a button because onLine is false. Use it to soften a message, nothing more.
How Do You Actually Know If the Server Is Reachable?
By trying to reach it. The only honest signal is the outcome of a real request, so build your connectivity model on request results rather than on a browser flag.
In practice that means each request reports one of four things: it succeeded, the server answered with an error, it timed out, or it failed at the transport level. Those four are meaningfully different to a user, and collapsing them into "something went wrong" throws away the information they need.
Give requests an explicit timeout rather than relying on the browser default, which can be very long. A user staring at a spinner for forty seconds will reload the page, and a reload during an unacknowledged write is how duplicate records get created.
Where Should Unsent Work Live?
On the device, immediately, before the request is attempted. The rule we follow is that nothing the user typed exists only in memory. As soon as a form field changes, or a draft is edited, it is written to local storage on the device.
That single decision removes most of the pain. If the request fails, the data is still there. If the tab crashes, the data is still there. If the user closes the laptop lid on a train, the data is still there when they open it.
It also changes what you can promise in the interface. "Saved locally, will sync when you are back online" is a true statement you can make confidently. "Saving..." for ninety seconds is not.
Can the Browser Retry for You After the User Leaves?
Sometimes, and it is worth knowing the limits. The Background Synchronization API exists for exactly this: MDN describes it as enabling "a web app to defer tasks so that they can be run in a service worker once the user has a stable network connection," and says the sync event fires "as soon as the network becomes available."
The requirements are a secure context over HTTPS and a service worker. The bigger caveat is support. MDN labels it "Limited availability" with the note that the feature "is not Baseline because it does not work in some of the most widely-used browsers."
So treat background sync as an enhancement, not a foundation. Build a retry queue that works in the page, then let background sync improve it where it exists. A design that only works with background sync will silently not work for a meaningful share of your users.
Should You Adapt the Interface to Connection Speed?
Cautiously. The Network Information API is designed for this, and MDN describes it as providing "information about the system's connection in terms of general connection type (e.g., 'wifi', 'cellular', etc.)," usable "to select high definition content or low definition content based on the user's connection."
The same caveat applies. Its specification lives with the Web Incubator Community Group, and MDN marks it "Limited availability" with the same note that it does not work in some of the most widely-used browsers.
Our position is that connection-aware behaviour should only ever remove weight, never remove function. Serving a lighter image when the API reports a slow connection is fine. Hiding a feature is not, because the same user on the same connection may be perfectly able to use it. The general approach is the one we set out in designing for slow connections.
How Do You Show State Without Lying?
Use three states per item, not two. Most interfaces have "saving" and "saved," which forces you to lie when neither is true. Add a third: pending.
Pending means we have your work, it is safe on this device, and it has not reached the server yet. Saved means the server confirmed it. Failed means we tried and stopped, and here is what you can do. Each state needs its own visual treatment and its own words, and none of them should be a spinner that never resolves.
Show the state per item rather than globally where you can. A single banner saying "offline" is less useful than knowing which three of your twelve edits have not landed. This is the same argument for granular feedback we made about long running jobs.
What Does a Good Retry Policy Look Like?
Automatic, backing off, bounded, and idempotent. Retry immediately once, then at growing intervals, then stop and hand control to the user. Do not retry forever, because an infinite retry loop on a genuinely rejected request is indistinguishable from a broken app.
The essential piece is idempotency. Every write your client can retry needs a client-generated identifier that the server uses to recognise a repeat. Without it, a retry after an unacknowledged success creates a duplicate, and duplicates are much worse than a failed save because the user cannot see that anything went wrong.
Send that identifier from the first attempt, not from the retry. The whole point is that the client cannot tell whether the first attempt landed.
What About Conflicts When Two Devices Both Had Changes?
Decide the rule before you build the feature, and make it visible. Last write wins is a legitimate choice for many products and a terrible one for others. What is never acceptable is a silent overwrite the user cannot detect.
For most B2B tools the pragmatic answer is field-level merging where changes do not overlap, and an explicit prompt where they do. The prompt does not need to be clever. It needs to show both versions and let a person choose, and it needs to keep the losing version recoverable.
That recoverability is the part people cut for time, and it is the part that turns a bad moment into a catastrophe. Keeping a way back is the same principle as making destructive actions undoable.
How Do You Test Any of This?
Deliberately, because it will not happen during a demo. Three things belong in your routine. First, throttle the connection in browser devtools and use the app normally, not just load it. Second, use the offline toggle mid-action, after clicking save but before the response. Third, and most usefully, block your own API domain while leaving the rest of the internet working, which reproduces the case navigator.onLine gets wrong.
That third test is the one that finds real bugs, because it is the real-world scenario: the device is online, your server is unreachable. If your interface says "you are offline" in that state, it is wrong, and a user will waste ten minutes checking their wifi.
Write the expected behaviour down for each of the three tests before you run them. Otherwise you will look at a broken state and rationalise it, which is easy to do when you know how the code works. The wording you land on should follow the same rules as any other failure message, which we covered in designing error messages.
What Is the Minimum Worth Doing?
Persist input locally on change, give every request a timeout, add a per-item pending state, and make your writes idempotent. Four things, none of them exotic, and together they remove the failure modes that make users distrust a product.
Everything beyond that, full offline support, service worker caching, background sync, conflict merging, is a real project and should be justified by real usage. Plenty of B2B tools do not need it. Almost none of them can afford to lose what a user typed.
If you are building something people will use on unreliable connections and want it designed properly the first time, we are happy to help. You can reach us at phoenix.studio.
Want a site that performs like this?
Tell us about your project. We will come back with a clear next step, no pressure.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.
Have a project like this?
Tell us where you want to go. We'll tell you how we'd get you there.