Skip to content

refactor(qwik-router): typed fail() failures on .error, error() aborts to the error page#8717

Draft
maiieul wants to merge 26 commits into
QwikDev:build/v2from
maiieul:v2-fail-error-rework
Draft

refactor(qwik-router): typed fail() failures on .error, error() aborts to the error page#8717
maiieul wants to merge 26 commits into
QwikDev:build/v2from
maiieul:v2-fail-error-rework

Conversation

@maiieul

@maiieul maiieul commented Jun 11, 2026

Copy link
Copy Markdown
Member

What is it?

  • Feature / enhancement

Description

Reworked take on #8714 after researching how other meta-frameworks handle this. return fail(status, data) comes back as the way to signal expected failures — it surfaces on a typed, reactive loader.error / action.error (a ServerError with the payload fields flat and on .data), and .value stays success-only. fail() payload types are inferred into .error and unioned with zod$/valibot$ error types, so the error channel is fully typed end to end. throw error() keeps its v1 meaning and aborts to the error page (on SPA paths: loaders fall back to a full-page load, action submit() rejects). New isServerError() guard for narrowing on the client, where instanceof doesn't survive deserialization.

The rewrite() URL changes were split into #8718.

maiieul added 8 commits June 10, 2026 20:54
…`fail()`

Route loaders and actions now report failures via a typed `.error` (a `ServerError`) instead of `fail()`/`value.failed`. `throw error(...)` and failed validators surface on `loader.error`/`action.error` consistently on server and client; `.value` is the success type only. `ServerError` exposes its payload fields directly (`error.fieldErrors`), typed from the validator schema, with `.data` kept as the canonical payload. Same-origin absolute `rewrite()` now normalizes to a relative rewrite instead of erroring.

BREAKING CHANGE: `requestEvent.fail()`, the `FailReturn` type, and `value.failed` are removed. Throw `error(status, data)` and read `loader.error`/`action.error` instead.
…result

requestEv.fail(status, data) returns a FailReturn<T> branded with a unique
symbol (non-enumerable, JSON-unspoofable, unlike v1's failed:true check).
Constructor overloads split the return union: .value = ExcludeFail<OBJ>,
.error = ServerError<FailPayload<OBJ> | validator errors>. Pure producer:
status applies at conversion time, so an unreturned fail() is a no-op.

Runtime conversion to the .error state lands in the next commit.
…to abort semantics

One conversion choke point (failToServerError + applyFailureResponse):
validator failures and returned fail() results become the loader/action
.error state with status + Cache-Control hygiene; thrown error() and
unexpected errors propagate to middleware again (v1 abort semantics).
Multi-loader failure status is deterministic (first in registration
order). The q-loader endpoint keeps its 200 {e} envelope but never
caches failures. resolveValue rejects when the depended-on loader
failed. Failed loaders are memoized like successes.
… error types

Thrown error() on JSON paths now ships an abort envelope: loaders fall
back to a full-page load (server renders the real error page; GETs are
safe to replay), actions reject run()/submit() and record no state —
<Form> surfaces aborts via the submitcompleted detail instead (avoids
unhandled rejections). Fixes the {e,s}-vs-{error} envelope mismatch that
silently swallowed middleware errors. ActionReturn/FormSubmitCompletedDetail
gain error (value now optional); LoaderSignal.error is honestly typed
ServerError<ERROR> | Error (client transport failures); isServerError()
narrows structurally so it works across serialization boundaries.
RewriteMessage carries search separately so it can't be percent-encoded
into the pathname. An explicit query on the rewrite target replaces the
request's query; otherwise the original is kept. Fragments are dropped
(they never reach the server). Absolute-URL detection now requires a
protocol instead of startsWith('http'), which matched relative paths
like /http-docs.
…erate API docs

Action/route-loader/validator pages rewritten for return-fail()-to-.error
plus throw-error()-aborts; error-handling and complex-forms pages fixed
(were teaching the removed model); v1→v2 upgrade section added. Changesets
rewritten. api.update run; ae-forgotten-export warnings fixed by exporting
FailOfRest/ValidatorReturn*/ServerError from the runtime entry (removes
the machine-absolute path from the checked-in api.md).
Fixtures use return fail() for expected failures (typed .error, inline
UI, status + no Cache-Control) and throw error() for intentional error
pages (plugin@errors interception restored). New loader-fail fixture and
specs: inline 429 rendering, SPA full-page fallback on thrown loader
errors. Unit tests added for rewrite() branches (8) and multi-loader
failure status determinism (2).
@maiieul maiieul requested review from a team as code owners June 11, 2026 07:21
@changeset-bot

changeset-bot Bot commented Jun 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bf44c78

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@qwik.dev/router Major
eslint-plugin-qwik Major
@qwik.dev/core Major
create-qwik Major
@qwik.dev/react Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@maiieul maiieul self-assigned this Jun 11, 2026
@maiieul maiieul moved this to Waiting For Review in Qwik Development Jun 11, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jun 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

@qwik.dev/core

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/core@8717

@qwik.dev/router

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/router@8717

eslint-plugin-qwik

npm i https://pkg.pr.new/QwikDev/qwik/eslint-plugin-qwik@8717

create-qwik

npm i https://pkg.pr.new/QwikDev/qwik/create-qwik@8717

@qwik.dev/optimizer

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/optimizer@8717

commit: bf44c78

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor
built with Refined Cloudflare Pages Action

⚡ Cloudflare Pages Deployment

Name Status Preview Last Commit
qwik-docs ✅ Ready (View Log) Visit Preview bf44c78

@maiieul maiieul marked this pull request as draft June 11, 2026 08:13
@maiieul maiieul moved this from Waiting For Review to In progress in Qwik Development Jun 11, 2026
maiieul added 3 commits June 11, 2026 10:30
The same-origin absolute URL normalization and query/fragment handling
for rewrite() are orthogonal to the failure-model redesign — extracted
to their own PR. rewrite() keeps the build/v2 behavior here (absolute
URLs are rejected with a 400).
@maiieul maiieul force-pushed the v2-fail-error-rework branch from 151cf07 to 5239c62 Compare June 11, 2026 09:43
maiieul added 6 commits June 11, 2026 17:29
…re unions

action.error?.fieldErrors typechecks without narrowing when validator
and fail() payloads mix; missing keys are typed ?: never (v1's .value
ergonomics, now on .error).
Restores the v1 capability of reading failed action/loader state in
document head functions: ResolveSyncValue is typed
Awaited<T> | ServerError<StrictUnion<ERROR>> (| undefined for actions)
and failed loaders return their error instead of throwing 'Loader not
executed'. Narrow with isServerError().
Plain property checks discriminate success vs failure in head() — no
isServerError needed.
The transport-failure Error member carries the server failure's fields
as ?: never, so loader.error?.status / payload fields typecheck and
discriminate without isServerError. Guard demoted to catch-block edges
in the docs.
maiieul added 6 commits June 11, 2026 22:56
…e level

Type pins: truthy status narrows to the ServerError member (payload
non-optional inside the branch). New SPA e2e: an aborted q-loader fetch
lands a plain Error on loader.error and the non-status branch renders.
…n state, type collapses

q-loader failure envelopes no longer get max-age re-applied after the
hygiene delete; abort envelopes scrub Cache-Control. Aborted/bailed
submissions reset isNavigating; non-JSON action responses settle as
aborts instead of hanging submit() forever. Aborts record no state
(status included). PE POSTs keep the action's failure status over a
loader's. LoaderSignal no longer collapses to never for always-failing
loaders; head() resolveValue returns loader errors instead of throwing;
StrictUnion restored on .value; ServerError exported as a value from
the runtime entry; TransportError exported (api.md path leak gone).
New tests: SPA-nav fail() envelope e2e (status 200, uncached, reactive
429), real-path resolveValue rejection, never-collapse pin.
…rError flattening

Aborts (thrown error() / unexpected server errors) now reject the
submit()/run() promise unconditionally — the client mirror of the
server-side throw. <Form> catches its own invocation's rejection and
keeps surfacing aborts via submitcompleted detail.aborted. ServerError
no longer flattens reserved payload keys: error.message is always a
string, payloads can't spoof status/data or replace the instance
prototype, and the flat type mirrors this with Omit. login fixture
gates the generic error block on the typed union instead of message
falsiness. New abort e2e fixture + spec, server-error unit tests.
… submit-rejection contract

isServerError (minor) and always-reject-on-abort (patch) move to
follow-up PRs for a separate release. Interim abort contract: a
programmatic submit() rejects; <Form> surfaces aborts via
submitcompleted detail.aborted. The ServerError reserved-key hardening
stays (patch changeset).
maiieul added 3 commits June 12, 2026 18:31
…ssions

detail.aborted moves to a follow-up minor; the abort delivery plumbing
(envelope, rejection for programmatic submit, state reset) stays as the
fix for aborts silently resolving as successes.
…elope caching, value export move to follow-ups
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

1 participant