Better error handling
This commit is contained in:
@@ -702,3 +702,56 @@ future model's peak lands awkwardly relative to the 0.1 fraction).
|
||||
is no real crash-telemetry feedback loop. If field reports show 0.1 is still
|
||||
too generous (or too aggressive) for some device class, adjust the constant
|
||||
rather than the shape of the heuristic.
|
||||
|
||||
## D-030 — Error taxonomy: pattern-matched classification, not structured error types
|
||||
**Status:** Accepted (2026-07-29)
|
||||
Every failure path (decode, model load, transcription, a crashed worker, a
|
||||
live session) used to end at `setError(rawMessage)`, rendered as one plain
|
||||
string — no distinction between "this file won't decode" (try another file),
|
||||
"the tab ran out of memory" (try a smaller model), a lost WebGPU device
|
||||
(switch backend), or a dropped network connection mid-download (retry). The
|
||||
WorkBreakdown M6 item asked for exactly those four categories.
|
||||
**Shape:** `src/lib/errors.ts` exports a pure `classifyError(raw, context)`:
|
||||
`context.stage` (`"decode" | "load" | "transcribe" | "worker-crash"`) plus
|
||||
`context.offline` (`navigator.onLine`) disambiguate cases the message text
|
||||
alone can't, then substring pattern-matching on the message sorts the rest
|
||||
into `unsupported-file` / `out-of-memory` / `device-lost` / `network` /
|
||||
`generic`, each with a user-facing `message`, an optional actionable
|
||||
`suggestion`, and `retryable`. **Why pattern-matching, not typed errors from
|
||||
each layer:** the failures cross Web Audio, Transformers.js, onnxruntime-web
|
||||
(WASM and the WebGPU/WebNN JSEP EPs), and `fetch` — none of which expose a
|
||||
shared error hierarchy the app controls, so there is nothing to `instanceof`
|
||||
against; the only thing every layer reliably produces is an `Error` (or
|
||||
DOMException) with a message string. A worker crash (`Worker.onerror`) with
|
||||
an **empty** message is itself classified as out-of-memory: D-027 found that
|
||||
the browser's silent OOM tab-kill fires no JS error event with details at
|
||||
all, so an empty message *is* the OOM signal in that one context — the
|
||||
classifier only applies that inference when `context.stage ===
|
||||
"worker-crash"`, not to an empty message anywhere else.
|
||||
**UI wiring (`src/App.tsx`):** the `error` state changed from a `string` to
|
||||
`{ classified: ClassifiedError; retryKind?: "load" | "file" }`. `retryKind`
|
||||
is resolved to an actual retry action **at render time** (`onClick={() =>
|
||||
startLoad(modelId, devicePreference)}` / replaying `lastFileRef.current`
|
||||
through `handleFile`), reading current component state directly — not
|
||||
computed inside the worker's `message`/`error` event listeners, which are
|
||||
registered once per worker lifetime (`ensureWorker`) and would otherwise
|
||||
close over whichever render's `modelId`/`devicePreference` happened to be
|
||||
current at registration, going stale on every later model/device switch.
|
||||
**Consequence:** only `network` and `device-lost` are `retryable` (an
|
||||
unsupported file or an OOM won't succeed by retrying the identical action);
|
||||
the error box shows a Retry button only for those, alongside the suggestion
|
||||
text. Live-mode errors (mic permission, WS disconnects) still classify
|
||||
through the same function for a consistent error box, but never carry
|
||||
`retryKind` — the existing Start/Stop controls are already the retry path
|
||||
there.
|
||||
**Alternatives rejected:** wrapping each subsystem's calls in typed error
|
||||
classes at the boundary (real fix, but Transformers.js/onnxruntime-web
|
||||
throw plain `Error`s from deep internals — retrofitting types there means
|
||||
forking dependencies, out of scope); classifying in the worker instead of
|
||||
the UI (the worker already forwards a string message over postMessage —
|
||||
classifying there would just move the same pattern-matching one hop over
|
||||
for no benefit, and the UI is where `context.offline`/retry actions live).
|
||||
**Revisit:** if a future onnxruntime-web/Transformers.js version starts
|
||||
surfacing richer structured errors (e.g. a `GPUDeviceLostInfo` object
|
||||
instead of a stringified message), prefer inspecting that directly over
|
||||
adding more substring patterns.
|
||||
|
||||
@@ -218,8 +218,14 @@ Notes (M5):
|
||||
exercised for real (browser half still needs the manual run).
|
||||
|
||||
## M6 — Hardening & polish
|
||||
- [ ] Error taxonomy: unsupported file, out-of-memory (suggest smaller model),
|
||||
WebGPU device-lost, network loss mid-download (resume or clear retry)
|
||||
- [x] Error taxonomy: unsupported file, out-of-memory (suggest smaller model),
|
||||
WebGPU device-lost, network loss mid-download (resume or clear retry) —
|
||||
pure `classifyError` in `src/lib/errors.ts` (D-030) sorts every failure
|
||||
path (decode, load, transcribe, a crashed worker) into one of the four
|
||||
categories plus a generic fallback, each with a user-facing message and
|
||||
an actionable suggestion; the error box in `App.tsx` adds a Retry
|
||||
button for the two retryable kinds (network, device-lost), replaying
|
||||
the failed load or the last file. Unit-tested in `tests/errors.test.ts`.
|
||||
- [x] Cancel in-flight transcription (both engines) — browser engine via
|
||||
worker termination (D-018), server engine via aborting the
|
||||
`/api/transcribe` fetch (D-018 amendment; landed with M5's EngineToggle)
|
||||
|
||||
Reference in New Issue
Block a user