Frontend data layer: silent update failures + dead/unreachable mutation error strings #63

Closed
opened 2026-06-08 13:42:12 +00:00 by logaritmisk · 1 comment
Owner

Severity: High. From a frontend deep audit, 2026-06-08. Two issues in the TanStack Query mutation layer that silently lose user-facing error signal.

Problems

  • [High] Term/authority updates fail silently. useUpdateTerm (web/src/api/queries.ts:444) and useUpdateAuthority (:521) lack meta.suppressErrorToast that every sibling mutation has — and their consumers term-row.tsx/authority-row.tsx never read update.isError. Result: depending on the path, a failed update produces no inline alert and/or no toast. The create forms render an isError alert; the update/inline-edit rows do not.
  • [High] Generic mutation error strings are unreachable. ~10 mutations throw new Error("update failed" | "create failed" | "rename failed" | …) (queries.ts:184,203,279,331,382,441,475,518,562), but mutationErrorMessage (query-client.ts:11-23) only inspects InUseError, HttpError 503, and meta.errorMessage — everything else collapses to t("toast.error"). So those bespoke strings never reach the UI, and a 403/500/422 on an update is indistinguishable in the toast. The 422 field-rejection that flows as FieldRejection on the create→setFields path is not surfaced on useUpdateObject (:203).
  • [Med] object-edit-form mislabels fetch errors as "not found". objects/object-edit-form.tsx:17-24 destructures only { data, isLoading }; on a failed fetch (network/500, not 404) it falls through to the not-found branch. Compare object-detail.tsx:34-46 which handles isLoading → isError → !object.

Suggested fixes

  • Apply one error-feedback policy uniformly to term/authority updates: keep suppressErrorToast and add an inline isError alert in the row (matching the create-form pattern), or drop suppressErrorToast so the global toast fires.
  • Make mutation errors load-bearing: throw new HttpError(response.status) (or attach status) so mutationErrorMessage can branch; or delete the dead strings to remove the false signal. Consider surfacing FieldRejection on useUpdateObject's 422 like the create path.
  • Add the isError branch to object-edit-form.

Source: frontend deep audit (data-layer dimension), 2026-06-08.

**Severity: High.** _From a frontend deep audit, 2026-06-08. Two issues in the TanStack Query mutation layer that silently lose user-facing error signal._ ## Problems - **[High] Term/authority updates fail silently.** `useUpdateTerm` (`web/src/api/queries.ts:444`) and `useUpdateAuthority` (`:521`) lack `meta.suppressErrorToast` that every sibling mutation has — **and** their consumers `term-row.tsx`/`authority-row.tsx` never read `update.isError`. Result: depending on the path, a failed update produces no inline alert *and/or* no toast. The create forms render an `isError` alert; the update/inline-edit rows do not. - **[High] Generic mutation error strings are unreachable.** ~10 mutations `throw new Error("update failed" | "create failed" | "rename failed" | …)` (`queries.ts:184,203,279,331,382,441,475,518,562`), but `mutationErrorMessage` (`query-client.ts:11-23`) only inspects `InUseError`, `HttpError` 503, and `meta.errorMessage` — everything else collapses to `t("toast.error")`. So those bespoke strings never reach the UI, and a 403/500/422 on an update is indistinguishable in the toast. The 422 field-rejection that flows as `FieldRejection` on the create→`setFields` path is **not** surfaced on `useUpdateObject` (`:203`). - **[Med] `object-edit-form` mislabels fetch errors as "not found".** `objects/object-edit-form.tsx:17-24` destructures only `{ data, isLoading }`; on a failed fetch (network/500, not 404) it falls through to the not-found branch. Compare `object-detail.tsx:34-46` which handles `isLoading → isError → !object`. ## Suggested fixes - Apply one error-feedback policy uniformly to term/authority updates: keep `suppressErrorToast` **and** add an inline `isError` alert in the row (matching the create-form pattern), or drop `suppressErrorToast` so the global toast fires. - Make mutation errors load-bearing: `throw new HttpError(response.status)` (or attach status) so `mutationErrorMessage` can branch; or delete the dead strings to remove the false signal. Consider surfacing `FieldRejection` on `useUpdateObject`'s 422 like the create path. - Add the `isError` branch to `object-edit-form`. _Source: frontend deep audit (data-layer dimension), 2026-06-08._
Author
Owner

Fixed in merge 56076c4.

Grounding corrected the framing: the update mutations weren't silentuseUpdateTerm/useUpdateAuthority were the only 2 of 18 mutations without suppressErrorToast, so they fired a disconnected global toast while their create/delete siblings showed inline errors. The fix makes all surfaces consistent and status-aware:

  • Single source of truth: api/error-message.ts errorMessageKey(error) maps InUseError (→ count) and HttpError by status (403 forbidden / 404 notFound / 409 conflict / 422 validation / ≥500 server) → i18n keys, fallback toast.error. Used by the global toast (query-client.ts) and every inline site.
  • Shared <MutationError error> component renders the inline alert (or nothing) and replaces the duplicated form.rejected markup at the create/rename forms, the term/authority edit rows, and (via the helper) the delete dialog + object-form formError.
  • Mutations throw HttpError(status) (16 sites) so the status reaches the mapping; InUseError/FieldRejection branches preserved.
  • Edit-row consistency: useUpdateTerm/useUpdateAuthority now suppress the toast and show an inline message at the row, staying editable on failure, with mutation.reset() clearing stale errors on re-edit.
  • Fetch mislabel fixed: object-edit-form distinguishes a fetch error (objects.loadError) from "not found" (isError guard before the !object guard).

5 new errors.* i18n keys (en/sv parity). 247 tests pass; typecheck/lint/build clean; check:size 216.2 KB gz; check:colors clean; no new dependency; no codename.

Out of scope → follow-ups: structured field-level rejection on core-object 422 (uncertain backend shape); relocating the error classes to api/errors.ts (tracked in #65); delete-object-dialog.tsx/publish-control.tsx keep their own form.rejected/visibility-error handling by design.

Fixed in merge `56076c4`. Grounding corrected the framing: the update mutations weren't *silent* — `useUpdateTerm`/`useUpdateAuthority` were the only 2 of 18 mutations **without** `suppressErrorToast`, so they fired a disconnected global toast while their create/delete siblings showed inline errors. The fix makes all surfaces consistent **and** status-aware: - **Single source of truth:** `api/error-message.ts` `errorMessageKey(error)` maps `InUseError` (→ count) and `HttpError` by status (403 forbidden / 404 notFound / 409 conflict / 422 validation / ≥500 server) → i18n keys, fallback `toast.error`. Used by the global toast (`query-client.ts`) **and** every inline site. - **Shared `<MutationError error>` component** renders the inline alert (or nothing) and replaces the duplicated `form.rejected` markup at the create/rename forms, the term/authority edit rows, and (via the helper) the delete dialog + object-form `formError`. - **Mutations throw `HttpError(status)`** (16 sites) so the status reaches the mapping; `InUseError`/`FieldRejection` branches preserved. - **Edit-row consistency:** `useUpdateTerm`/`useUpdateAuthority` now suppress the toast and show an inline message at the row, staying editable on failure, with `mutation.reset()` clearing stale errors on re-edit. - **Fetch mislabel fixed:** `object-edit-form` distinguishes a fetch error (`objects.loadError`) from "not found" (`isError` guard before the `!object` guard). 5 new `errors.*` i18n keys (en/sv parity). 247 tests pass; typecheck/lint/build clean; check:size 216.2 KB gz; check:colors clean; no new dependency; no codename. **Out of scope → follow-ups:** structured field-level rejection on core-object 422 (uncertain backend shape); relocating the error classes to `api/errors.ts` (tracked in #65); `delete-object-dialog.tsx`/`publish-control.tsx` keep their own `form.rejected`/visibility-error handling by design.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: logaritmisk/biggus-dickus#63