You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Updated 2026-08-04 to match the converged v2 error-API design: PublicError is removed from the model; server-origin failures are always redacted, client-origin errors never are. The original motivation ("connectivity failures get redacted") no longer applies — the surviving motivation is that transport failures need a type. The class below is now standalone.
Server-origin failures never cross the wire. The client receives a framework-authored generic error (+ digest); the original stays server-side (logs, onError). No exceptions — server-authored displayable content exists only as outcomes (httpError(), invalid(), redirects) or typed returns.
Client-origin errors are never redacted. Their messages come from code already in the browser, so rendering them is leak-safe by construction.
Failures land in .error (guarded) or the closest <ErrorBoundary> (unguarded).
Problem
A user goes offline and SPA-navigates. The loader data fetch rejects at the transport level, and that raw rejection is what lands in the failure channel. Raw is leak-safe, but it is not an offline UX:
The message is browser-divergent trivia: Failed to fetch (Chrome), Load failed (Safari), NetworkError when attempting to fetch resource. (Firefox). Nothing to branch on, nothing a user should read.
The useful affordance (offline notice, retry, "showing cached data") needs a type, not a string. (Raised by @wmertens.)
And the fix cannot be server-side leniency, because the redaction membrane must stay absolute. Unexpected err.message values in production routinely name infrastructure — and they fire exactly during incidents (credential rotation, network partition) when no app code changed:
Source
Real production err.message
pg (auth failure, 28P01)
password authentication failed for user "admin"
Node net layer
connect ECONNREFUSED 10.0.3.7:5432
Node DNS
getaddrinfo ENOTFOUND db.internal.corp
mysql2
Access denied for user 'admin'@'10.0.2.14' (using password: YES)
Prisma P1000
Authentication failed against database server at `10.0.3.7`, the provided database credentials for `admin` are not valid
Prisma P1001
Can't reach database server at `10.0.3.7`:`5432
AWS SDK (IAM)
User: arn:aws:iam::123456789012:user/app-server is not authorized to perform: s3:GetObject on resource: …
undici / Node 18+ fetch
message is fetch failed, but err.cause carries connect ECONNREFUSED 10.0.3.7:443
Connectivity failures were never "unexpected" in the first place: the framework can prove what they are, because the framework owns the fetch. No inference, no consent problem.
Proposal: a framework-constructed NetworkError
// @qwik.dev/core — vocabulary, not machinery; usable router-lessexportclassNetworkErrorextendsError{constructor(){super('Could not reach the server');// framework-authored message}}
Standalone class — no parent, no serdes, no special cases. It rides the ordinary failure channels (.error / boundary); instanceof is the entire API.
// router fetch layer (loader data fetches, server$ client stub) — sketchtry{response=awaitfetch(url,{ signal, headers });}catch(e){if((easError)?.name==='AbortError')throwe;// cancellation stays cancellationthrownewNetworkError();// transport-level rejection ONLY}// an HTTP response is NOT a network error — the server answered; those paths are unchanged
Rules
Placement: class in core (re-exported from the router for discoverability); wrap sites in the router (loader data fetch layer, server$ client stub, and the batched transport when/if it lands).
Wrap scope: only framework-owned, transport-level rejections (offline, DNS, CORS-opaque, timeout). AbortError stays cancellation. Any HTTP response — error envelopes, 422, 500-no-detail — keeps its existing path.
Prefetch failures stay silent: prefetching is speculative; a failed prefetch must not construct a NetworkError, populate .error, or touch a boundary. The nav-time fetch retries naturally. (Otherwise walking through a tunnel makes hovered links light up error UI.)
Semantics = client connectivity only: NetworkError means "this browser could not reach the server". A server-side upstream failure during SSR is a different situation (the user's connection is fine) — docs steer that to throw httpError(503, …) or a typed return. Framework-constructed instances are client-side only and never cross the wire, so instanceof NetworkError just works with no serializer support; a copy constructed server-side is a failure like any other throw → redacted.
Fetch-layer hygiene: a rejected coalesced fetch rejects all registered consumers with the same NetworkError, once; a transport failure teaches any transport-level caching/hints nothing (no response, no headers).
App-owned fetches (e.g. inside async computeds) get the one-line recipe: catch the transport rejection, throw new NetworkError() — same class, same fallbacks.
What it enables
First paint while offline — the boundary displays it, and the fallback can finally branch:
<ErrorBoundaryfallback$={(err)=>{if(errinstanceofNetworkError){return<div>You appear to be offline. <buttononClick$={retry}>Retry</button></div>;}// server failures arrive redacted — render your own copy (+ digest for support)return<p>Something went wrong.</p>;}}><Suspensefallback={<Skeleton/>}><Orders/></Suspense></ErrorBoundary>
Failed background refresh — with the retention semantics (a failed revalidation keeps the held value and surfaces on .error; reading .error is the guard that unlocks .value), the offline-first story falls out for free:
constorders=useOrders();// e.g. { poll: 30_000 }return(<div>{orders.errorinstanceofNetworkError&&<Badge>Offline — showing cached data</Badge>}<ul>{orders.value.map((o)=><likey={o.id}>{o.title}</li>)}</ul></div>);
Task-side observation (toast, no content replacement):
useTask$(({ track })=>{if(track(()=>orders.error)instanceofNetworkError){toasts.push('Connection lost — data may be stale');}});
(Modeled server$ failures are typed returns under the settled design, so they never reach this catch.)
Tests
Router unit: transport rejection → NetworkError; AbortError excluded; HTTP responses excluded; prefetch failure is silent; coalesced rejection fans out once.
E2E: offline SPA nav → boundary shows the instanceof NetworkError branch; offline background refresh → retained value + .error badge, boundary never fires; offline server$ call → try/catch receives an instanceof NetworkError.
Scope
Follow-up to #8745 — not part of the EB PR. Depends on the error-model rework landing (server-side redaction membrane + .error/boundary routing). PublicError removal is tracked in the EB workstream.
Note
Updated 2026-08-04 to match the converged v2 error-API design:
PublicErroris removed from the model; server-origin failures are always redacted, client-origin errors never are. The original motivation ("connectivity failures get redacted") no longer applies — the surviving motivation is that transport failures need a type. The class below is now standalone.What is it?
Context: the settled error model
onError). No exceptions — server-authored displayable content exists only as outcomes (httpError(),invalid(), redirects) or typed returns..error(guarded) or the closest<ErrorBoundary>(unguarded).Problem
A user goes offline and SPA-navigates. The loader data fetch rejects at the transport level, and that raw rejection is what lands in the failure channel. Raw is leak-safe, but it is not an offline UX:
Failed to fetch(Chrome),Load failed(Safari),NetworkError when attempting to fetch resource.(Firefox). Nothing to branch on, nothing a user should read.And the fix cannot be server-side leniency, because the redaction membrane must stay absolute. Unexpected
err.messagevalues in production routinely name infrastructure — and they fire exactly during incidents (credential rotation, network partition) when no app code changed:err.messagepg(auth failure, 28P01)password authentication failed for user "admin"connect ECONNREFUSED 10.0.3.7:5432getaddrinfo ENOTFOUND db.internal.corpmysql2Access denied for user 'admin'@'10.0.2.14' (using password: YES)Authentication failed against database server at `10.0.3.7`, the provided database credentials for `admin` are not validCan't reach database server at `10.0.3.7`:`5432User: arn:aws:iam::123456789012:user/app-server is not authorized to perform: s3:GetObject on resource: …fetch failed, buterr.causecarriesconnect ECONNREFUSED 10.0.3.7:443Connectivity failures were never "unexpected" in the first place: the framework can prove what they are, because the framework owns the fetch. No inference, no consent problem.
Proposal: a framework-constructed
NetworkErrorStandalone class — no parent, no serdes, no special cases. It rides the ordinary failure channels (
.error/ boundary);instanceofis the entire API.Rules
server$client stub, and the batched transport when/if it lands).AbortErrorstays cancellation. Any HTTP response — error envelopes, 422, 500-no-detail — keeps its existing path.NetworkError, populate.error, or touch a boundary. The nav-time fetch retries naturally. (Otherwise walking through a tunnel makes hovered links light up error UI.)NetworkErrormeans "this browser could not reach the server". A server-side upstream failure during SSR is a different situation (the user's connection is fine) — docs steer that tothrow httpError(503, …)or a typed return. Framework-constructed instances are client-side only and never cross the wire, soinstanceof NetworkErrorjust works with no serializer support; a copy constructed server-side is a failure like any other throw → redacted.NetworkError, once; a transport failure teaches any transport-level caching/hints nothing (no response, no headers).throw new NetworkError()— same class, same fallbacks.What it enables
First paint while offline — the boundary displays it, and the fallback can finally branch:
Failed background refresh — with the retention semantics (a failed revalidation keeps the held value and surfaces on
.error; reading.erroris the guard that unlocks.value), the offline-first story falls out for free:Task-side observation (toast, no content replacement):
server$— imperative, the caller decides:(Modeled
server$failures are typed returns under the settled design, so they never reach thiscatch.)Tests
NetworkError;AbortErrorexcluded; HTTP responses excluded; prefetch failure is silent; coalesced rejection fans out once.instanceof NetworkErrorbranch; offline background refresh → retained value +.errorbadge, boundary never fires; offlineserver$call →try/catchreceives aninstanceof NetworkError.Scope
Follow-up to #8745 — not part of the EB PR. Depends on the error-model rework landing (server-side redaction membrane +
.error/boundary routing).PublicErrorremoval is tracked in the EB workstream.