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

106
frontend/src/types/api.ts Normal file
View File

@@ -0,0 +1,106 @@
/**
* Typed API error contracts and models.
*
* Provides discriminated error types for standardized error handling across
* API calls and hooks, enabling actionable error reporting and diagnostics.
*/
/**
* Discriminated error type representing an HTTP API error response.
*
* Thrown when the server returns a non-2xx HTTP status code.
* Use the `type` discriminator to handle different error categories.
*/
export interface ApiErrorPayload {
type: "api_error";
/** HTTP status code returned by the server. */
status: number;
/** Raw response body text as returned by the server. */
body: string;
/** User-friendly error message derived from status and body. */
message: string;
}
/**
* Discriminated error type representing a network error.
*
* Thrown when the request fails due to network issues (DNS lookup failure,
* connection timeout, offline, CORS error, etc.) or when parsing JSON fails.
*/
export interface NetworkErrorPayload {
type: "network_error";
/** Underlying error message (network stack or JSON parse error). */
message: string;
}
/**
* Discriminated error type representing a request abort.
*
* Thrown when a fetch request is aborted (e.g., component unmounts, user cancels).
* These are expected and should typically be silently ignored.
*/
export interface AbortErrorPayload {
type: "abort_error";
message: string;
}
/**
* Union of all possible fetch error types.
*
* Use the `type` discriminator to narrow and handle each error case:
*
* ```ts
* const error: FetchError = ...;
* if (error.type === "api_error") {
* // Handle HTTP error — check status for 401/403/50x etc.
* if (error.status === 401) redirectToLogin();
* } else if (error.type === "network_error") {
* // Handle network/connectivity issues
* showNetworkMessage();
* } else if (error.type === "abort_error") {
* // Request was cancelled — typically silent
* return;
* }
* ```
*/
export type FetchError = ApiErrorPayload | NetworkErrorPayload | AbortErrorPayload;
/**
* Type guard to check if an error is an authentication error (401/403).
*
* @param error - The error to check (typically a FetchError)
* @returns `true` if the error is a 401 or 403 API error
*/
export function isAuthError(error: FetchError): error is ApiErrorPayload {
return error.type === "api_error" && (error.status === 401 || error.status === 403);
}
/**
* Type guard to check if an error is an abort error (request cancelled).
*
* @param error - The error to check (typically a FetchError)
* @returns `true` if the error is an abort error
*/
export function isAbortError(error: FetchError): error is AbortErrorPayload {
return error.type === "abort_error";
}
/**
* Type guard to check if an error is a network error.
*
* @param error - The error to check (typically a FetchError)
* @returns `true` if the error is a network error
*/
export function isNetworkError(error: FetchError): error is NetworkErrorPayload {
return error.type === "network_error";
}
/**
* Type guard to check if an error is an API error.
*
* @param error - The error to check (typically a FetchError)
* @returns `true` if the error is an API error
*/
export function isApiError(error: FetchError): error is ApiErrorPayload {
return error.type === "api_error";
}