feat: Implement typed error contracts in generic hooks

Introduce discriminated FetchError union type to replace weak string error
handling in API calls and hooks. Enables actionable error diagnostics.

Changes:
- Create types/api.ts with FetchError discriminated union (api_error,
  network_error, abort_error)
- Export type guards: isAuthError, isAbortError, isNetworkError, isApiError
- Update useListData and usePolledData to expose typed FetchError instead of
  string
- Add getErrorMessage() helper to extract displayable messages from FetchError
- Add createStringErrorAdapter() for backward compatibility with string error
  state
- Update handleFetchError() to work with both FetchError and string setters
- Update all consumer hooks to expose typed errors
- Update components to use getErrorMessage() when displaying errors
- Update tests to mock FetchError instead of strings
- Add comprehensive typed error model documentation to Web-Development.md

This enables better error handling patterns:
- Check error.type to distinguish between API, network, and abort errors
- Extract status codes for specific handling (401/403 auth, 50x server errors)
- Maintain backward compatibility with existing string-based error states

All TypeScript compilation passes with no errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-28 09:13:47 +02:00
parent 6c8e2b3423
commit 5166789b68
45 changed files with 531 additions and 125 deletions

View File

@@ -990,6 +990,104 @@ export function useSaveData() {
- Log errors to the console (or a future logging service) with sufficient context for debugging.
- Always handle the **loading**, **error**, and **empty** states for every data-driven component.
### Typed Error Model
All API errors are normalized into a discriminated `FetchError` union type. This replaces generic error strings with **actionable, typed error information** that enables better diagnostics and UX.
**The three error types:**
```ts
// From src/types/api.ts
// Server returned HTTP error (non-2xx status)
interface ApiErrorPayload {
type: "api_error";
status: number; // 401, 403, 500, etc.
body: string; // Raw response body
message: string; // Formatted for display
}
// Network failure, DNS lookup failure, JSON parse error, etc.
interface NetworkErrorPayload {
type: "network_error";
message: string;
}
// Request was cancelled (component unmount, user abort, etc.)
interface AbortErrorPayload {
type: "abort_error";
message: string;
}
// Union of all three
type FetchError = ApiErrorPayload | NetworkErrorPayload | AbortErrorPayload;
```
**Usage in hooks:**
```ts
import { useListData } from "./useListData";
import type { FetchError } from "../types/api";
import { isAuthError, isApiError } from "../types/api";
export function useBans(): UseBansResult {
const { items, loading, error, refresh } = useListData<BanListResponse, Ban>({
fetcher,
selector,
errorMessage: "Failed to load bans",
});
// error is now FetchError | null, not string | null
// Use the type discriminator to handle different cases:
if (error?.type === "api_error") {
if (error.status === 401 || error.status === 403) {
// Already handled globally by AuthProvider, but you can check specifically
} else if (error.status >= 500) {
// Server error — suggest retry or contact support
}
} else if (error?.type === "network_error") {
// Network connectivity issue — show offline-friendly message
} else if (error?.type === "abort_error") {
// Request was cancelled — typically silent (component unmounted)
}
return { items, loading, error, refresh };
}
```
**Extracting displayable messages:**
```ts
import { getErrorMessage } from "../utils/fetchError";
// Convert any FetchError to a user-friendly string:
if (error) {
const message = getErrorMessage(error);
// Abort and auth errors return empty string (silently handled)
// Other errors return error.message
if (message) {
showNotification(message);
}
}
```
**Backward compatibility with string-based error state:**
If a component still uses `useState<string | null>` for errors, use the adapter:
```ts
import { handleFetchError, createStringErrorAdapter } from "../utils/fetchError";
const [errorMessage, setErrorMessage] = useState<string | null>(null);
fetch()
.catch((err: unknown) => {
// Adapt the FetchError to string setter
handleFetchError(err, createStringErrorAdapter(setErrorMessage), "Failed");
});
```
### Error Boundaries — Granular Fallback Strategy
React error boundaries catch render-time exceptions and allow graceful fallback UI instead of a full white screen crash. BanGUI implements a **three-level error boundary strategy** to balance resilience with UX clarity: