benchmarks
BuilderProof editorial team18 min read31 views

What Happens When the Connection Drops? A Proposed Axis for Client Network Failure Handling (September 2026)

A proposed benchmark axis for what a generated app does when the user’s own connection fails mid-action. The HTML Standard defines navigator.onLine so that true means "might be online", and the Fetch Standard defines a failed request as a response with status zero, no headers and no body. Seven weighted signals, four postures, a ten-step protocol, and no scores.

Updated on September 20, 2026

Flat editorial diagram on a pale background: a slate blue rounded rectangle standing for a browser card with a thin grey bar inside it, and a terracotta dashed curve leaving its right edge and breaking off before it reaches two small circles.
Flat editorial diagram on a pale background: a slate blue rounded rectangle standing for a browser card with a thin grey bar inside it, and a terracotta dashed curve leaving its right edge and breaking off before it reaches two small circles.
On this page

Quick Answer

Client network failure handling is a proposed BuilderProof benchmark axis, drafted September 20, 2026, that scores what a generated application does when the user's own connection fails partway through an action. Not the server failing, and not a third party failing. The person using the application loses signal in a lift, on a train, or behind a captive portal, and the question is whether the application can tell them what happened to their work.

We have not scored it. This is a pre-registration of a rubric so that it can be attacked before any number is attached to a vendor name.

The finding that motivates it is narrow and checkable. Every browser primitive a generated application could reach for to answer that question documents its own incapacity, in its own specification, in its own words. None of them is broken. Nothing composes them, and the composition is the thing the user needs.

Why client network failure deserves its own axis

A generated application is normally exercised by one person on one connection: the person who prompted it into existence, sitting at a desk, usually on the same machine that is serving it. On that connection a request either succeeds or comes back with a status code. The third outcome, where the request simply does not come back and the application cannot tell whether it arrived, does not occur, cannot be made to occur by accident, and has to be induced deliberately with a tool nobody reaches for while building.

So the defect is not rare. It is unobservable during construction, which is a different and more specific claim.

What "client network failure handling" means here

The axis scores the deployed artifact and its source. It asks, of the untouched build:

When the user's connection fails while an action is in flight, does the application tell them what happened to their work, and is the work still there?

Four boundaries keep this honest.

It measures the client side of the request, not the server side. The server may be perfectly correct and still be irrelevant here, because the failure happens before the response reaches the browser. The party left in doubt is the user, and the only surface available to resolve that doubt is the interface in front of them.

It measures what the application does, not whether the network is good. A slow or lossy connection is not a defect in the artifact. Behaviour under that connection is.

It measures behaviour under interruption. An application whose connection never fails cannot be scored. The protocol below requires induced failure, which is what makes this axis cost more to run than reading a response header.

It is not a demand for an offline-first application. Most generated applications should not maintain a local replica. A build that fails honestly, preserves what the user typed and says clearly that nothing was saved can score well here without storing a single byte offline.

The finding: four layers, four documented limits, no composition

To answer "did my change reach the server, and is my typing safe", an application has four primitives available. Each one is documented. Each documentation says, in some form, that the primitive cannot do the job on its own.

The detection layer defines its own answer as a maybe

The obvious first move is to ask the browser whether it is online. The HTML Living Standard defines that property asymmetrically, and states the asymmetry plainly:

WHATWG HTML Standard, on navigator.onLine: "Returns false if the user agent is definitely offline (disconnected from the network). Returns true if the user agent might be online."

WHATWG One of the two values is a fact and the other is a guess, and the guess is the one an application reads when it is deciding whether to let the user press Save.

MDN is blunter about why, and about what follows:

MDN, Navigator: onLine: "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. Therefore, 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."

MDN That is the reference documentation for the API instructing readers not to build behaviour on it. A build that gates its save button on navigator.onLine has not merely chosen a weak signal. It has chosen the one signal whose own documentation says do not do this.

The transport layer defines failure as an empty object

The second move is to catch the failure and read it. There is nothing to read.

MDN, Window: fetch(): "A fetch() promise only rejects when the request fails, for example, because of a badly-formed request URL or a network error. A fetch() promise does not reject if the server responds with HTTP status codes that indicate errors (404, 504, etc.)."

Two consequences follow, and they point in opposite directions. A server error arrives as a perfectly ordinary fulfilled promise, so an application that only writes a catch block never notices a 500 at all. And a rejection carries no detail, because the specification defines the failure object as empty:

WHATWG Fetch Standard: "A network error is a response whose type is 'error', status is 0, status message is the empty byte sequence, header list is empty, body is null."

Status zero, no headers, no body. That is not an oversight, it is the definition. MDN's own list of what raises the resulting TypeError includes an invalid URL, a URL containing credentials, invalid values in the options object, a request blocked by a permissions policy, and a network error, all as one exception type with one name. So the signal that means the train went into a tunnel is the same signal that means a developer typed the URL wrong.

This is the seam. MDN documents what the developer observes. The Fetch Standard documents why there is nothing to observe.

The preservation layer is conditional, generic and unreliable

If the application cannot tell what happened, the next best thing is to not lose the user's typing. The browser event most often reached for carries three documented restrictions in a row:

MDN, Window: beforeunload event: "Require sticky activation for the dialog to be displayed. In other words, the browser will only show the dialog box if the frame or any embedded frame receives a user gesture or user interaction.", "Only show a generic browser-specified string in the displayed dialog. This cannot be controlled by the webpage code.", and "It is not reliably fired, especially on mobile platforms."

Conditional on interaction, incapable of saying what is at risk, and least reliable on exactly the devices whose connections fail most often.

The replay layer is not available everywhere

The cleanest remedy is to hand the write to something that will deliver it later. That mechanism exists and is not portable:

MDN, Background Synchronization API: "Limited availability. This feature is not Baseline because it does not work in some of the most widely-used browsers."

A remedy that works for some of a build's users is a reasonable engineering choice. It is not a substitute for the application knowing what happened.

The honest counterweight

None of these four is a defect in the platform. Every one of them is documented accurately by the people who wrote it, and two of them, keepalive requests and the online and offline events, are genuinely useful primitives supplied for free. The gap this axis proposes to measure is in application code, which is where the four have to be composed and where nothing instructs anyone to compose them.

The proposed rubric

Seven signals, weights summing to 100.

Scroll to see more

SignalWeightWhat a failing case looks like
An interrupted write is distinguishable from a rejected one22One message, "Something went wrong", covers both a request that never left the device and a request the server may already have applied
Unsent user input survives the failure20The error render replaces the form, the component unmounts, and twenty minutes of typing is gone with no way back
The retry affordance is safe to press16A retry button reissues a non-idempotent write that may already have landed, creating a second row or a second charge
Failure detection does not rest on navigator.onLine alone14The application reports itself online while every request fails, because the device is on a network that does not reach the internet
Recovery is offered where the work is12The user is sent to a full-page error screen, and the route that held their draft is gone
A failed read is distinguishable from an empty result9A list or a search returns nothing and renders "No results found", so a connection failure reads as a confident factual answer
Restored connectivity is noticed without a manual reload7Signal returns and the interface stays in its failed state until the user thinks to refresh

The four postures

Level 0, Unhandled. The rejection is uncaught, or caught into a single generic message. Typed input lives only in component state and is destroyed when the error path renders. This is not negligence so much as the default: it is what the code looks like when nobody decided anything, and on a good connection it is indistinguishable from correct.

Level 1, Announced. The failure is caught and the user is told. Real work, and it removes the worst outcome, which is a silent nothing. The message does not distinguish failure classes, the retry is unconditional, and whether the typing survives is incidental.

Level 2, Preserved. The user's unsent work survives and the interface returns them to it. At minimum the application separates "this never left the device" from "this may have arrived", and says which it is. This is the step that changes whether anyone loses anything.

Level 3, Resumable. The write carries a client-generated key so that retrying is safe whether or not the first attempt landed, restored connectivity is noticed without a manual reload, and a failed read is never rendered as an empty collection.

The step from Level 0 to Level 1 is the only one the user can see immediately. The step from Level 1 to Level 2 is the only one that changes whether work is lost. We think that asymmetry, rather than the number of levels, is the useful thing here, and we expect it to be the part most worth arguing about.

How to reproduce it

  1. Build the reference application from a fixed prompt. A form that saves a record, and a list that displays the saved records. The brief says nothing about connectivity, retries, drafts or offline behaviour. Asking for them measures whether the builder can follow an instruction, which is a different axis.
  2. Deploy it. A development server on the same machine has a network path that does not fail in the way this axis is about.
  3. Looks-correct control. On a working connection, fill the form, submit it, and confirm the record saves and appears in the list. Everything measured afterwards is about failure. Without this step a broken build reads as a failing posture.
  4. The never-left case. Type into the form. Do not submit. Sever the network interface. Submit. Record exactly what the interface shows and where the typed text is.
  5. Restore the interface without reloading the page. Record whether the application notices.
  6. Reload the page. Record whether the typed input survived.
  7. The in-doubt case. Restore the connection and repeat, but this time sever it after the request has left the device and before a response returns. Record whether the interface distinguishes this from step 4. Restore, reload, and count the rows.
  8. The retry test. Press whatever retry affordance the interface offers, twice. Count the rows again.
  9. The read case. Sever the interface and perform a read: load the list, or type into any search or filter the build provides. Record whether the failure renders as an error or as an empty collection.
  10. Repeat steps 4 and 7 on a mobile browser with the tab backgrounded, because that is the environment in which the preservation layer is documented as least reliable.

Steps 4, 7 and 9 are three different failures that can produce the same screen. Reporting them as one number would discard the finding.

The named trap: the wired-desk illusion

Every observation available while an application is being built comes from one actor on one connection, and that connection works. The developer is the network. The illusion is not that failures are rare. It is that failure is legible: that a request either succeeds or returns a readable error, and that whatever the application does in the second case must therefore be adequate.

The specifications say otherwise. A failed request is defined as a response with status zero, no headers and no body. That is the whole of what the application gets, and a build cannot be observed handling it well or badly without someone deliberately pulling the cable.

This is the twelfth trap in this series to share a one-actor structure, and it belongs with the seeded corpus, where the only search corpus the box has ever been tested against was typed by the person testing it. Both are cases where the tester supplies the very condition that hides the defect. Here the connection between the two is more than structural: a search request that fails on a bad connection commonly renders as "No results found", so a network failure is presented to the user as a confident factual answer about their data. That is signal 6 above, and it is the point at which the two axes touch.

How this relates to our existing axes

Four neighbours touch this territory closely enough that the boundaries have to be stated, and two of them are in genuine tension with it.

It is not our realtime subscription correctness axis. That axis states its own boundary in a sentence that is exactly the hole this one fills: it "assumes the write landed perfectly and asks whether a reader who was not connected at that moment ever finds out". This axis is about the write that did not land, and about the writer rather than the reader. The scores move independently in both directions. A build can converge perfectly after every disconnection and still discard a form submission that failed while the user was in a tunnel, because a subscription never observes a write that was never made. A build can preserve and replay that submission flawlessly and still show a frozen list for an hour.

It is not our outbound call failure axis, although the two share the idea of a request in doubt. They sit on opposite sides of the boundary. That axis is our server calling somebody else, where the in-doubt party is the server and the remedy is an idempotency key held by code we control. This axis is the browser calling us, where the in-doubt party is a person and the only remedy surface is the interface. Both directions hold: a server with perfect idempotency still leaves a user who never received a response unable to know whether to try again, and a browser that retries safely cannot make a non-idempotent endpoint safe to retry against.

It is not our state-handling completeness axis, and this is the sharpest boundary in the set because the two are most easily confused. That axis asks whether an error state exists and is rendered well. This one asks whether the application can tell which failure it is, and whether the user's work survived it. A generic "Something went wrong" with a retry button scores full marks there and can sit at Level 0 here, having discarded the form. The reverse holds too: a build could preserve the draft perfectly and render no error state at all.

And it is a documented tension with that same axis, not merely a boundary. That rubric's recovery row awards its top score for a "Retry, refresh, or fallback offered inside the state". Our outbound call failure axis exists because retrying an in-doubt non-idempotent write is precisely what duplicates its effect. So one of our own rubrics rewards the affordance that another of our own rubrics identifies as the defect. We do not think either row is wrong. The resolution is ordering: the retry is correct once the application can distinguish step 4 from step 7, and premature once it cannot. Signal 3 above exists to carry that distinction, and we would rather state the conflict than quietly weight around it.

It is not our concurrent-write safety axis, which asks whether two overlapping writers both keep their effect. That axis is writer against writer. This one is a single writer who does not know whether their own write happened. There is a second tension here. That axis awards its Level 1, Client-guarded, for an interface that "disables the submit button while a request is in flight", which is real defensive work against an accidental double submit. Under this axis's conditions, a request that never resolves leaves that button disabled permanently, and the correct remedy there becomes a lockout here. The resolution is scope: the guard should be bounded by a deadline rather than by the arrival of a response, which is a requirement neither axis states on its own.

What we are not claiming

We are not claiming that any builder in the cohort ships this defect. We have not scored this axis, and the proposal exists so the rubric can be attacked first.

We are not claiming that any of the four specifications is at fault. The HTML Standard, the Fetch Standard and MDN all document their limits accurately, and in several cases supply the primitive an application needs. The gap is in the code that does or does not pick them up.

We are not claiming every application needs Level 3. A read-only dashboard has almost nothing at stake in a dropped request. What the axis measures is whether the handling matches what the interface is asking the user to trust it with.

Limitations and open questions

Induced failure is not one thing. Severing an interface, throttling to a very slow profile, and a captive portal that answers every request with its own login page are three different conditions, and the third is the one that defeats detection most completely while looking online to every layer. Our protocol covers the first two properly and the third not at all, which is a real gap.

The in-doubt case is hard to induce reliably. Step 7 requires cutting the connection inside a window that may be a few hundred milliseconds wide. We do not have a crisp recipe for making that window reliable, and a rubric whose sharpest signal rests on the least repeatable step is a rubric with a problem.

Inherited behaviour is hard to attribute. A builder whose default data-fetching library retries and caches will look better here for reasons unrelated to generated code quality, in the same way library reconnection complicates our realtime axis. We have not decided whether to treat that as inherited or as credit.

The weights are a first draft. In particular, 9 points for distinguishing a failed read from an empty result may be too low, given that its failure mode is an application making a confident false statement about the user's own data rather than merely failing to act.

We have not measured the cohort. Everything above is read from specifications and reference documentation. Documentation is evidence of design intent, not of behaviour on a given run, and closing that gap is what the protocol is for.

Corrections, counterexamples from real deployed builds, and rubric edits are welcome. The most useful thing you can send us is a step 6 observation from your own generated application: what was in the form after you reloaded it.

References

Read in full on September 20, 2026.

Cite this benchmark

Plain text
BuilderProof editorial team. "What Happens When the Connection Drops? A Proposed Axis for Client Network Failure Handling (September 2026)". BuilderProof, September 2026. https://www.builderproof.org/benchmarks/what-happens-when-the-connection-drops-client-network-failure-axis-september-2026.
BibTeX
@misc{builderproof-what-happens-when-the-connection-drops-client-network-failure-axis-september-2026,
  title  = {{What Happens When the Connection Drops? A Proposed Axis for Client Network Failure Handling (September 2026)}},
  author = {{BuilderProof editorial team}},
  year   = {2026},
  month  = {sep},
  howpublished = {\url{https://www.builderproof.org/benchmarks/what-happens-when-the-connection-drops-client-network-failure-axis-september-2026}},
  note   = {BuilderProof, builderproof.org}
}

Frequently asked questions

What is client network failure handling in an AI-generated app?

It is a proposed BuilderProof benchmark axis, drafted September 20, 2026, that scores what a generated application does when the user’s own connection fails partway through an action, rather than when the server or a third party fails. It covers whether an interrupted write is distinguishable from a rejected one, whether unsent input survives, whether the retry affordance is safe to press, whether failure detection rests on navigator.onLine alone, where recovery is offered, whether a failed read is distinguishable from an empty result, and whether restored connectivity is noticed without a manual reload. No builder has been scored on it.

Why can a generated app not just check navigator.onLine?

Because the property is defined asymmetrically and its own documentation says not to build behaviour on it. The WHATWG HTML Living Standard states that it returns false if the user agent is definitely offline and true if the user agent might be online, so only one of the two values is a fact. MDN adds that on Windows the status is determined by whether the device can reach a Microsoft home server, which a firewall or VPN may block even when the internet is reachable, and concludes that the property is inherently unreliable and should provide hints rather than gate features.

Why is a failed fetch so hard to diagnose in the browser?

Because the specification defines the failure as empty. The WHATWG Fetch Standard defines a network error as a response whose type is error, whose status is zero, whose status message is the empty byte sequence, whose header list is empty and whose body is null. MDN adds that a fetch promise rejects only when the request itself fails and does not reject on HTTP error statuses such as 404 or 504, and that the resulting TypeError also covers an invalid URL, a URL containing credentials, invalid options and a request blocked by a permissions policy. One exception type, many causes, no detail.

How is this different from an axis about retries or realtime updates?

Our outbound call failure axis is our server calling somebody else, where the party left in doubt is the server and the remedy is an idempotency key in code we control. This axis is the browser calling us, where the party left in doubt is a person and the only remedy surface is the interface. Our realtime subscription correctness axis states in its own scope note that it assumes the write landed perfectly and asks whether a disconnected reader ever finds out; this axis is about the write that did not land, and about the writer rather than the reader.

Methodology

Realtime Subscription Correctness: A Proposed Axis for Whether a Generated Live View Ever Notices It Stopped Being Live (September 2026)

A candidate BuilderProof benchmark axis scoring whether a generated application's live views converge back to the true server state after the connection carrying their updates is interrupted. From the database notification layer upward, every delivery primitive is documented as reaching whoever is connected at that instant, with no backlog for anyone who was not, and every recovery mechanism is opt-in.

19 min read55
Methodology

State-handling completeness: a proposed benchmark axis for AI app builders (August 2026)

State-handling completeness is a proposed BuilderProof benchmark axis (August 2026) that scores how well an AI app builder generates the non-ideal runtime states of the apps it produces: loading, empty, and error states. It is a 20-point axis across five sub-criteria, measured reproducibly by giving all five commercial builders (v0, Lovable, Replit, Base44, Bolt.new) an identical fixed prompt and then inspecting the generated app under a throttled network, an empty account, and a forced request failure.

10 min read163