diff --git a/docs/error-handling-conventions.md b/docs/error-handling-conventions.md new file mode 100644 index 0000000..e8859e9 --- /dev/null +++ b/docs/error-handling-conventions.md @@ -0,0 +1,789 @@ +# Error Handling Conventions + +> Issue #630 — Establish the expected pattern for every error category +> (boundary, query state, mutation feedback, transaction surface, network +> warning) so the UI feels consistent regardless of which component rendered +> the error. + +This guide is the **single source of truth** for how errors are surfaced in +the Access Layer client. Pair it with +[Error Handling in React Query Hooks](./error-handling-in-hooks.md) for the +hook-side `ApiError` branching detail and +[State Management](./state-management.md) for the +[Error Architecture Strategy](./state-management.md#3-error-architecture-strategy-boundary-vs-inline-states) +section that motivates the boundary/inline split. + +--- + +## The Five Error Display Modes + +Every error in the client surfaces through **one of five modes**. Pick the +right one based on where the error came from and what the user still needs +to be able to do. + +| # | Mode | Trigger | Audience | Component (canonical) | +| --- | -------------------- | --------------------------------------- | --------------- | ----------------------------------------------------- | +| 1 | **Toast** | `useMutation.onError` (user action) | Glanceable | `showToast.error` from `@/utils/toast.util` | +| 2 | **Inline state** | `useQuery` `isError` (blocking content) | Focused | `CreatorProfileErrorState`, section-level cards | +| 3 | **Section boundary** | Render-time throw in a sub-component | Isolated retry | `SectionErrorBoundary` from `@/components/common` | +| 4 | **Page boundary** | Render-time throw on a whole page | Whole route | `CreatorPageErrorBoundary`, `AppErrorBoundary` | +| 5 | **Tx surface** | On-chain trade write failure | Detail-oriented | `TransactionRetryNotice` + `TransactionFailureDrawer` | + +**Plus one always-on overlay** for a precondition state: + +| # | Mode | Trigger | Audience | Component | +| --- | ------------------ | ----------------------------- | ----------------- | -------------------------------------------------- | +| 6 | **Network banner** | Connected wallet, wrong chain | Persistent notice | `NetworkMismatchBanner` from `@/components/common` | + +--- + +## Decision Flowchart + +Walk this top to bottom. The first matching branch wins. + +``` +Did the error come from a user-initiated write (buy, sell, enroll, claim, …)? +├── YES → Mode 1 (Toast) — see "Mode 1: Toasts" +│ Special: On-chain writes → also open Mode 5 drawer for detail +└── NO → Did it come from a useQuery that blocks the page? + ├── YES → Mode 2 (Inline state) — see "Mode 2: Inline" + └── NO → Did something throw while rendering JSX? + ├── YES inside a sub-component → + │ Mode 3 (SectionErrorBoundary) + ├── YES at the route level → + │ Mode 4 (Page boundary) + └── NO → Re-check the trigger; you may have an + unhandled case — default to Mode 4 (the last + line of defense is `AppErrorBoundary`). + +Network mismatch? → Mode 6 banner (always-on; renders nothing when healthy). +``` + +> **Cross-link.** The same boundary vs inline split is described from the +> loading-state angle in +> [State Management → 3. Error Architecture Strategy](./state-management.md#3-error-architecture-strategy-boundary-vs-inline-states). +> This document is the full convention; that section is the executive summary. + +--- + +## Mode 1 — Toasts for Mutation Errors + +**Use when:** + +- The failure came from a user-initiated write (buy, sell, course enroll, + profile update, share, copy). +- The page can still render usefully without retrying. +- One short line of feedback is enough — the user needs to know "this + didn't work" and either retry or change input. + +**Don't use when:** + +- The failure blocks the primary purpose of the screen (use Mode 2). +- The failure contains field-level validation detail that must attach to a + specific input (use Mode 2 inline errors with `apiError.response?.errors`). +- You need to retry automatically — toasts disappear, they don't retry. + +### Pattern: `useMutation.onError` + +Cast the error to `ApiError` and branch on `status`. The numeric scale is +documented in +[Error Handling in Hooks → Distinguishing Error Types](./error-handling-in-hooks.md#distinguishing-error-types); +here is the consolidated decision: + +```ts +// src/hooks/useBuyCreatorKey.ts +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { ApiError } from '@/services/api.service'; +import showToast from '@/utils/toast.util'; +import { queryKeys } from '@/lib/queryKeys'; +import { creatorKeysService } from '@/services/creatorKeys.service'; + +export function useBuyCreatorKey() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + creatorId, + amount, + }: { + creatorId: string; + amount: number; + }) => creatorKeysService.buyKey(creatorId, amount), + + onError: error => { + const apiError = error as ApiError; + + // 1. Network failure — user is offline / server unreachable + if (apiError.status === 0) { + showToast.error( + 'Network error. Check your connection and try again.' + ); + return; + } + + // 2. Server failure — not the user's fault; generic + retry + if (apiError.status >= 500) { + showToast.error( + 'The server ran into a problem. Please try again shortly.' + ); + return; + } + + // 3. Validation — surface the first field error if present + if (apiError.status === 422 && apiError.response?.errors?.length) { + showToast.error(apiError.response.errors[0].message); + return; + } + + // 4. Other 4xx — API message is safe for the user + showToast.error(apiError.message); + }, + + onSuccess: (_, { creatorId }) => { + queryClient.invalidateQueries({ + queryKey: queryKeys.creators.detail(creatorId), + }); + queryClient.invalidateQueries({ queryKey: queryKeys.wallet.all }); + showToast.success('Key purchased successfully!'); + }, + }); +} +``` + +### Pattern: User rejection (wallet signature) + +Wallet signatures can be rejected by the user mid-flow. Detect this with +the shared helper and skip the noisy generic toast: + +```ts +// src/hooks/useWalletConnection.ts (excerpt) +import { getSignatureErrorMessage } from '@/utils/errorHandling.utils'; +import showToast from '@/utils/toast.util'; + +onError: error => { + showToast.error(getSignatureErrorMessage(error)); + // getSignatureErrorMessage returns either: + // - WALLET_ERROR_COPY.SIGNATURE_REJECTED (user clicked "Cancel") + // - WALLET_ERROR_COPY.SIGNATURE_FAILED (other wallet-side failure) +}; +``` + +`getSignatureErrorMessage` is implemented in +[`src/utils/errorHandling.utils.ts`](./../src/utils/errorHandling.utils.ts) +and detects the EIP-1193 code `4001`, ethers' `ACTION_REJECTED`, and the +common message fragments "user rejected" / "declined" / "cancelled". + +### Pattern: Off-chain mutation that should NOT use a drawer + +Copy-to-clipboard, share-to-Twitter, follow a creator — all of these are +writes that have no chain side effects. Just toast: + +```ts +// Excerpt from CreatorProfileHeader +const handleShare = async () => { + try { + await navigator.clipboard.writeText(profileUrl); + showToast.success('Profile link copied to clipboard!'); + } catch { + showToast.error('Could not copy the profile link. Please try again.'); + } +}; +``` + +--- + +## Mode 2 — Inline Error State for Blocking Query Failures + +**Use when:** + +- The query is the primary content of the page (e.g. creator profile header). +- Without the data, the screen is empty; there is nothing else for the user + to do here. +- You want the user to retry without leaving the page — wire `refetch` from + React Query. + +**Don't use when:** + +- The query is enriching a page that can render something else (use Mode 3). +- The failure came from a mutation (use Mode 1). + +### Pattern: Canonical shared component + +`CreatorProfileErrorState` is the canonical inline error component. It +renders a marketplace-styled card with an icon, a message, and an optional +retry button. + +```tsx +// In a creator profile section +import { useCreatorProfile } from '@/hooks/useCreatorProfile'; +import CreatorProfileErrorState from '@/components/common/CreatorProfileErrorState'; +import { ApiError } from '@/services/api.service'; + +function CreatorHeader({ creatorId }: { creatorId: string }) { + const { data, isLoading, isError, error, refetch } = + useCreatorProfile(creatorId); + + if (isLoading) return ; + + if (isError) { + const apiError = error as ApiError; + return ( + void refetch()} + isRetrying={ + false /* wire from your query's isFetching if you want */ + } + title="Unable to load this creator profile" + /> + ); + } + + return ; +} +``` + +Component contract (from `CreatorProfileErrorStateProps`): + +| Prop | Type | Notes | +| ------------ | ------------------------- | ---------------------------------------------------------- | +| `error` | `Error \| string \| null` | Drives the message; falls back to a generic copy when null | +| `onRetry` | `() => void` | When provided, renders a retry button | +| `isRetrying` | `boolean` | Spins the refresh icon and disables the button | +| `title` | `string` | Defaults to "Unable to load this creator profile" | +| `message` | `string` | When present, overrides the error message | + +### Pattern: Hand-rolled inline state + +For sections that aren't creator profiles (holdings table, transaction +history), inline state is a styled `
` with a refresh +button wired to `refetch`. Always include the retry control — fail without +one and the user has no escape hatch. + +```tsx +const { data, isError, error, refetch } = useHoldings(address); + +if (isError) { + const apiError = error as ApiError; + return ( +
+

+ {apiError.status >= 500 + ? 'Unable to load holdings. Please try again later.' + : apiError.message} +

+ +
+ ); +} +``` + +### Inline vs. throw-to-boundary — the rule + +> If the failure should leave the rest of the page interactive → render an +> **inline** state. If it means the route as a whole cannot be useful → throw +> the error and let the page-level boundary catch it. + +Concretely: + +- A creator list failing inside the marketplace page? **Inline.** The hero, + filters, and holdings are still useful. +- The creator profile header failing on `/creator/:id`? **Throw** → + `CreatorPageErrorBoundary` renders a "Creator not found" or + "could not load" page-level state and a back-link. + +--- + +## Mode 3 — Section Error Boundary (Render Falls Here) + +**Use when:** + +- A sub-component (tab body, sidebar, list section) can throw during render + — e.g. it consumes data that is undefined when the parent didn't gate it. +- A failure in **this section** should not crash the whole page. +- The user should be able to retry the section without a full reload. + +**Don't use when:** + +- The error is a query failure (React Query surfaces it via `isError` — + branch in the component, don't throw). +- The error is from an on-chain mutation (use Mode 5). + +### Pattern: Wrap any sub-tree that might throw + +```tsx +import SectionErrorBoundary from '@/components/common/SectionErrorBoundary'; + +// In LandingPage + + +; +``` + +The retry button on the fallback resets the boundary's internal error +state; it does **not** re-fire the underlying `useQuery`. If you need a +network retry, combine the boundary with a query that has `enabled` tied +to a "retry counter" `useState` and bump it inside the retry handler. + +### Pair with `throwOnError` + +If you want React Query to throw a render-time exception (so the boundary +catches it) instead of branching on `isError` inside the component, opt in +on the query: + +```ts +useQuery({ + queryKey: queryKeys.creators.detail(id), + queryFn: () => courseService.getById(id), + throwOnError: error => { + const api = error as ApiError; + // 4xx: render inline; 5xx: bubble to boundary + return api.status >= 500; + }, +}); +``` + +See [Error Handling in Hooks → SectionErrorBoundary](./error-handling-in-hooks.md#use-sectionerrorboundary-when) +for the full reasoning. + +--- + +## Mode 4 — Page-Level Error Boundaries + +There are two nested page boundaries; pick the **most specific** one that +fits the route. + +### `CreatorPageErrorBoundary` — creator routes + +Scoped to `/creator/:id` and any other creator detail route. When a creator +page throws, this catches it and renders a fallback with a "back to +creators" link. It special-cases `ApiError` with `status === 404` to render +a "Creator not found" message instead of a generic failure. + +```tsx +// src/pages/CreatorDetailPage.tsx +import CreatorPageErrorBoundary from '@/components/common/CreatorPageErrorBoundary'; + +export default function CreatorDetailPage() { + const { id } = useParams<{ id: string }>(); + const { data, isLoading, error } = useCreatorDetail(id ?? ''); + + if (!data && !isLoading) { + throw new ApiError('Creator not found', 404); + } + if (error) { + throw error; // → CreatorPageErrorBoundary + } + + return ; +} + +// Wrap in the route element (already done in src/routes.tsx): +{routes}; +``` + +### `AppErrorBoundary` — last line of defense + +Mounted once in [`src/App.tsx`](./../src/App.tsx) around the router. When +something throws that isn't caught by a more specific boundary, this is +the last chance before React would otherwise unmount the entire tree. The +fallback offers a **full page reload** rather than resetting local state — +the rationale is documented in the component's source comment. + +Reach: anything that escapes a `SectionErrorBoundary` or +`CreatorPageErrorBoundary` will land here. + +--- + +## Mode 5 — Transaction Failure Surfaces + +Buy / sell / claim trades have **two** surfaces: an inline +`TransactionRetryNotice` so the user sees a persistent recovery prompt, +and a modal `TransactionFailureDrawer` for full diagnostic detail. + +### When to use both together + +A failed trade should: + +1. Show a `TransactionRetryNotice` in place of the trade action area — + it stays visible until the user retries or dismisses. +2. Open a `TransactionFailureDrawer` so the user can copy the error code + or transaction hash for support. + +```tsx +// src/components/common/CreatorCard.tsx (excerpt) +import TransactionRetryNotice from '@/components/common/TransactionRetryNotice'; +import TransactionFailureDrawer from '@/components/common/TransactionFailureDrawer'; +import type { TransactionFailureDetails } from '@/components/common/TransactionFailureDrawer'; + +const [failure, setFailure] = useState(null); + +const handleTrade = async (amount: number) => { + try { + await buyKey({ creatorId, amount }); + } catch (err) { + const apiError = err as ApiError; + setFailure({ + errorMessage: apiError.message, + errorCode: apiError.response?.code, + txHash: undefined, // chain-specific; pass when available + timestamp: Date.now(), + developerDetails: apiError.response, + }); + } +}; + +return ( + <> + {failure && ( + <> + { + setFailure(null); + handleTrade(lastAmount); + }} + /> + { + setFailure(null); + handleTrade(lastAmount); + }} + onDismiss={() => setFailure(null)} + /> + + )} + +); +``` + +`TransactionFailureDetails`: + +| Field | Type | Purpose | +| ------------------ | -------------------------- | ---------------------------------------------- | +| `errorMessage` | `string` (required) | User-facing message displayed in the drawer | +| `txHash` | `string?` | On-chain hash for support; copy-to-clipboard | +| `errorCode` | `string?` | Machine code (e.g. `INSUFFICIENT_BALANCE`) | +| `timestamp` | `number?` | Unix ms; rendered via `formatTimestampTooltip` | +| `developerDetails` | `Record?` | Hidden behind a `
` toggle for devs | + +See [`src/components/common/TransactionFailureDrawer.tsx`](./../src/components/common/TransactionFailureDrawer.tsx) +for the component contract. + +### When to NOT use the drawer + +If the action is not on-chain (share, copy, follow) — toast only. The +drawer exists to expose blockchain-detail information; if there is no +chain context, it adds noise. + +--- + +## Mode 6 — Network Mismatch Banner + +`NetworkMismatchBanner` is **always-on**: it reads from +`useNetworkMismatch()` and returns `null` when the wallet is on the +correct network. Mount it once in any layout where users can initiate +trades; it renders nothing in the happy path. + +```tsx +// Wherever the user can trade +
+ + +
+``` + +This is **not** an error per se — it's a precondition warning. Do **not** +also toast "wrong network" on top of this banner; the banner is the +canonical surface. + +--- + +## Branching on `ApiError.status` — Consolidated Cheat Sheet + +The same numeric scale drives every mode. Source: +[`src/services/api.service.ts`](./../src/services/api.service.ts) → +[`BaseApiService.handleError`](./../src/services/api.service.ts). + +| `apiError.status` | Meaning | Toast copy | Inline copy | Boundary fallback | +| ----------------- | ------------------------ | ---------------------------------------------------------- | ------------------------------------------- | ----------------------------------------------- | +| `0` | No network response | "Network error. Check your connection and try again." | "Check your connection and try again." | `AppErrorBoundary` | +| `400` | Malformed request | `apiError.message` (safe) | `apiError.message` | `AppErrorBoundary` | +| `401` | Session expired | (interceptor handles — auto-refresh, redirect to `/login`) | n/a | n/a | +| `403` | Insufficient permissions | `apiError.message` | "You don't have access to this." | `AppErrorBoundary` | +| `404` | Resource not found | `apiError.message` | "Not found." | `CreatorPageErrorBoundary` (404 special-cases) | +| `422` | Validation failure | `apiError.response.errors[0].message` (first field error) | Map every `response.errors[i]` to its input | `AppErrorBoundary` | +| `429` | Rate limited | "Too many requests. Please wait a moment and retry." | "Rate limited. Try again shortly." | `AppErrorBoundary` | +| `>= 500` | Server error | "The server ran into a problem. Please try again shortly." | "Unable to load this. Try again later." | Section-level (skeleton→empty→relies on inline) | + +--- + +## Adding a New Error Type to the Classification System + +The codebase has **two** classification helpers that capture domain +specifics. Pick the one that matches your domain; do not invent a new +helper unless neither fits. + +### 1. Wallet & signature errors → extend `WALLET_ERROR_COPY` + +File: [`src/utils/errorHandling.utils.ts`](./../src/utils/errorHandling.utils.ts). + +Centralized map of wallet-and-signature messages, plus detection helpers: + +```ts +export const WALLET_ERROR_COPY = { + SIGNATURE_REJECTED: + "Signature request was declined. Please try again when you're ready to confirm.", + SIGNATURE_FAILED: + 'The signature request failed. Please ensure your wallet is unlocked and try again.', + GENERIC_TRANSACTION_FAILED: + 'Transaction failed. Please check your balance or connection and try again.', +}; + +export function isUserRejection(error: unknown): boolean { + /* … */ +} +export function getSignatureErrorMessage(error: unknown): string { + /* … */ +} +``` + +**To add a new wallet error type:** + +1. Append a new key to `WALLET_ERROR_COPY` with a sentence that states the + cause and the next step. +2. If the detector matters, extend `isUserRejection`-style recognition in + a new exported predicate (`isRateLimited(error)`, `isChainSwitchRequired(error)`). +3. Wire the predicate + key into the calling hook's `onError` — usually + by extending `getSignatureErrorMessage` or introducing a sibling + `getWalletErrorMessage` if the surface is broader than signatures. +4. Add a Vitest unit test under + `src/utils/__tests__/errorHandling.utils.test.ts` that pins the new + copy and predicate behavior. + +### 2. Pre-action disabled reasons → extend the typed helper + +File: [`src/utils/claimActionDisabledReason.ts`](./../src/utils/claimActionDisabledReason.ts). + +Pattern: a **closed union** of reason keys + a **single `Record`** that +maps each key to a standardized copy. This pattern is reused for other +disabled-reason surfaces (see `BuyActionHelperText` and +`ClaimActionHelperText` documentation). + +```ts +export type ClaimActionDisabledReasonKey = + | 'wallet_not_connected' + | 'no_claimable_rewards' + | 'claim_in_progress' + | 'network_mismatch' + | 'insufficient_gas' + | 'unknown'; + +const CLAIM_ACTION_DISABLED_REASON_TEXT: Record< + ClaimActionDisabledReasonKey, + string +> = { + // Every entry follows the same shape: + // "{Action} is unavailable because {cause}. {Next step}." + wallet_not_connected: + 'Claim is unavailable because your wallet is not connected. Connect your wallet to continue.', + // … +}; + +export const getClaimActionDisabledReasonText = ( + reason: ClaimActionDisabledReasonKey +): string => CLAIM_ACTION_DISABLED_REASON_TEXT[reason]; +``` + +**To add a new disabled reason:** + +1. Append the new key to the union — **don't** fall back to `unknown` + silently; force every caller to make a decision. +2. Add a matching entry to `CLAIM_ACTION_DISABLED_REASON_TEXT` using the + "{Action} is unavailable because {cause}. {Next step}." template so + tones stay consistent. +3. Compute the new key at the call site (the component that evaluates + pre-conditions) and pass it to `getClaimActionDisabledReasonText`. +4. Extend + `src/utils/__tests__/claimActionDisabledReason.test.ts` to cover the + new key. + +### 3. When neither helper fits + +If your error surface is genuinely new (e.g. diverging-chain detection, +on-chain simulation errors), create a new sibling helper in +`src/utils/ErrorHandling.utils.ts` following the same shape: + +1. **Closed key union** — TypeScript exhaustiveness forces callers to + pick a copy. +2. **One Record/Map** that owns _all_ English copy — no copy scattered + across components. +3. **Pure functions** — no React, no hooks, no side effects. Easy to + unit-test. +4. **Side-by-side tests** — colocated unit test in + `src/utils/__tests__/`. + +Do not duplicate copy inline in components; always go through a typed +helper so future copy edits stay coherent. + +--- + +## Shared Error State Components — Reference + +| Component | Mode | Purpose | Key props | +| -------------------------------------------------- | ---- | --------------------------------------------------- | ------------------------------------------------------------------------- | +| `showToast` (`@/utils/toast.util`) | 1 | Mutation feedback (success, error, loading, tx) | `showToast.{success,error,loading,transactionSuccess}(message, options?)` | +| `CreatorProfileErrorState` (`@/components/common`) | 2 | Canonical inline error card for creator profile | `error?, onRetry?, isRetrying?, title?, message?` | +| `SectionErrorBoundary` (`@/components/common`) | 3 | Catches sub-tree render throws with retry | `sectionName?, minHeight?, className?` | +| `CreatorPageErrorBoundary` (`@/components/common`) | 4 | Catches creator-route render throws | none (wraps ``) | +| `AppErrorBoundary` (`@/components/common`) | 4 | App-wide last-line-of-defense; full reload on retry | none (wraps ``) | +| `TransactionRetryNotice` (`@/components/common`) | 5 | Persistent inline retry banner for failed trades | `title?, message, onRetry, retryLabel?, disabled?, className?` | +| `TransactionFailureDrawer` (`@/components/common`) | 5 | Modal with error code, hash, and developer details | `open, onOpenChange?, failureDetails, onRetry?, onDismiss?` | +| `NetworkMismatchBanner` (`@/components/common`) | 6 | Persistent "wrong network" warning | `className?` | + +For accessible state components (skeletons, empty states), see +[Adding a Page and Data Fetching](./adding-page-and-data-fetching.md) +and [Shared Components](./shared-components.md). + +--- + +## Worked Examples + +### Example A — Marketplace search list (read failure → Mode 2) + +The creator list inside the marketplace is critical content, so a query +failure renders an inline state with retry. The rest of the page +(hero, holdings, footer) stays interactive. + +```tsx +// src/pages/LandingPage.tsx (excerpt) +const { data: creators, isError, error, refetch } = useCreatorList(); + +if (isError) { + const apiError = error as ApiError; + return ( +
+

Couldn't load the creator list

+

+ {apiError.status >= 500 + ? 'Something went wrong on our end. Please try again.' + : apiError.message} +

+ +
+ ); +} + +return ; +``` + +### Example B — Creator profile (read failure → Mode 4) + +The creator profile header IS the page. A query failure here becomes a +page-level error: + +```tsx +// src/pages/CreatorDetailPage.tsx (excerpt) +function CreatorDetailPageContent() { + const { id } = useParams<{ id: string }>(); + const { data, isLoading, error } = useCreatorDetail(id ?? ''); + + if (isLoading) return ; + if (!data) throw new ApiError('Creator not found', 404); + if (error) throw error; // caught by CreatorPageErrorBoundary + return ; +} + +export default function CreatorDetailPage() { + return ( + + + + ); +} +``` + +### Example C — Buy flow (mutation failure → Mode 1 + Mode 5) + +A trade mutation has both the prompt (toast), the persistent retry +banner, and the detail drawer: + +```tsx +// src/components/common/CreatorCard.tsx (excerpt) +const buy = useBuyCreatorKey(); + +const handleBuy = async (amount: number) => { + try { + await buy.mutateAsync({ creatorId, amount }); + } catch (err) { + const apiError = err as ApiError; + + // Mode 1: quick prompt + showToast.error( + apiError.status === 0 + ? 'Network error. Check your connection.' + : apiError.message + ); + + // Mode 5: persistent banner + drawer for support detail + setFailure({ + errorMessage: apiError.message, + errorCode: apiError.response?.code, + timestamp: Date.now(), + }); + } +}; +``` + +--- + +## Quick Reference + +| Question | Answer | +| ----------------------------------------------------- | ----------------------------------------------------- | +| Failure from a button click? | Toast (Mode 1), add drawer for on-chain trades | +| Failure from `useQuery` that **is** the page? | Throw → Page boundary (Mode 4) | +| Failure from `useQuery` that **enriches** the page? | Inline state with `refetch` (Mode 2) | +| Component throws during render (not a query error)? | Wrap in `SectionErrorBoundary` (Mode 3) | +| Wallet is connected but wrong chain? | `NetworkMismatchBanner` (Mode 6) | +| User clicked "Cancel" on the wallet signature prompt? | `getSignatureErrorMessage` (Mode 1, specialised copy) | +| New kind of wallet error? | Extend `WALLET_ERROR_COPY` | +| New pre-action disabled reason? | Extend `ClaimActionDisabledReasonKey` + lookup | + +--- + +## Cross-references + +- [State Management](./state-management.md) — particularly + [§3 Error Architecture Strategy](./state-management.md#3-error-architecture-strategy-boundary-vs-inline-states) + for the boundary-vs-inline executive summary and the loading/error/data + three-state pattern. +- [Error Handling in React Query Hooks](./error-handling-in-hooks.md) — + `ApiError` shape, `useQuery` / `useMutation` patterns, and the full + status-code table. +- [API Layer Conventions](./api-layer.md) — the service layer and + `BaseApiService.handleError` that produces every `ApiError`. +- [React Query Cache Conventions](./react-query-cache-conventions.md) — + invalidation patterns that run alongside error handlers. +- [Shared Components](./shared-components.md) — toast, skeleton, and + empty-state families referenced alongside error states. +- [Adding a Page and Data Fetching](./adding-page-and-data-fetching.md) — + end-to-end guide for wiring routes and their error boundaries. +- [BuyActionHelperText — Disabled Reason](./BuyActionHelperText-DisabledReason.md) and + [Claim Action Disabled Reason Helper Text](./ClaimActionHelperText-DisabledReason.md) — + pre-action copy systems that follow the same `Record` + classification pattern documented above. diff --git a/docs/state-management.md b/docs/state-management.md index 698d63e..80a3394 100644 --- a/docs/state-management.md +++ b/docs/state-management.md @@ -89,6 +89,8 @@ Match your structural loading fallbacks strictly to your structural data card la - **Error Boundaries (`CreatorPageErrorBoundary`):** Use at the route level to safely isolate catastrophic runtime engine failures, critical layout state breakdowns, or complete backend authorization drops across whole pages. - **Inline Contextual States (`SectionErrorBoundary`):** Use for sub-components, standalone layout modules, tabs, or localized search bars where a remote service query issue shouldn't block a user from browsing the remainder of the active application canvas. Always supply the React Query context `refetch` callback method directly to retry controls. +> The full convention — every error display mode (toast, inline, section boundary, page boundary, transaction surfaces, network banner), the `ApiError` status cheat sheet, and how to add a new error type to the classification system — lives in **[Error Handling Conventions](./error-handling-conventions.md)**. + ### 4. Code Implementation Blueprint ```tsx