Error Handling Guide - striae-org/striae GitHub Wiki

Overview

This guide documents how error handling currently works in Striae across the React Router app and Cloudflare Workers.

It is intentionally implementation-aligned and avoids generic patterns that are not currently used in this repository.

Table of Contents

  1. Error Handling Principles
  2. Error Handling Layers
  3. Firebase Authentication Error Mapping
  4. Case Operation Messages
  5. Frontend Error Patterns
  6. API Client and Data Layer Patterns
  7. Worker and API Error Patterns
  8. Audit Logging Error Behavior
  9. Common Scenarios
  10. Current Gaps and Hardening Opportunities
  11. Developer Checklist
  12. Related Documentation

Error Handling Principles

Striae error handling follows these practical rules:

  • Show user-safe, actionable messages.
  • Keep the app usable when secondary operations fail.
  • Centralize Firebase auth error translation.
  • Validate network responses explicitly instead of assuming success.
  • Log development diagnostics without exposing sensitive details to users.

Error Handling Layers

1. Route-level fallback

The React Router root error boundary in app/root.tsx handles route exceptions and response errors.

  • Route response errors show status and status text.
  • Unexpected errors show a generic 500-style fallback message.

2. Auth/domain message mapping

Firebase auth errors are normalized through app/services/firebase/errors.ts.

  • ERROR_MESSAGES provides reusable user-facing strings.
  • handleAuthError maps Firebase error codes to those messages.
  • getValidationError returns standardized validation text by key.

3. Case operation messages

Case-level user-facing strings are centralized in app/utils/ui/case-messages.ts and re-exported through app/utils/ui/index.ts.

  • Covers import validation, export failures, deletion confirmations, read-only case operations, and data integrity messages.
  • Static constants for fixed text and functions for dynamic text (e.g. case number interpolation).
  • Prevents message drift across the multiple components that handle case workflows.

4. API client layer

API clients in app/utils/api/ throw structured errors for authentication and request failures rather than returning error objects.

  • Callers are expected to catch these in try/catch blocks.
  • Common thrown messages include authentication token failures and HTTP status descriptions.

5. Permission result objects

Permission checks in app/utils/data/permissions.ts return structured result objects instead of throwing.

  • Pattern: { allowed: boolean; reason?: string } (or { canCreate: boolean; reason?: string }).
  • On internal errors the functions return { allowed: false } with a generic reason string (fail-closed).
  • Callers check the result rather than catching exceptions.

6. Component-level state

Most UI flows keep local error and success state, for example in login, MFA, password reset, profile, and email action handlers.

Common behavior:

  • Clear stale errors before starting a new async action.
  • Set loading state for async operations.
  • Use try/catch/finally to keep UI state consistent.
  • Clear errors when users begin corrective input.

7. Worker/API boundary

Workers return HTTP status codes with JSON or plain-text bodies.

Most workers (audit, data, image, PDF) now return consistent JSON bodies with an error field. The user worker still returns plain-text error bodies.

App-side callers are expected to:

  • Check response.ok.
  • Read/parse response safely (JSON with text fallback).
  • Surface a friendly message or fallback error string.

Firebase Authentication Error Mapping

Current mapping is implemented in app/services/firebase/errors.ts.

Mapped categories include:

  • Credential and account errors (invalid credential, user not found, disabled user, password mismatch).
  • Email action errors (invalid/expired action code, invalid continue URL).
  • MFA errors (invalid code, expired code, too many requests, missing verification state, TOTP setup errors).
  • Recent-login and operation restriction errors.

This allows auth components to avoid direct Firebase-code branching in most UI messaging paths.

Case Operation Messages

Case-level user-facing messages are centralized in app/utils/ui/case-messages.ts and re-exported through app/utils/ui/index.ts.

This module provides consistent text for case management workflows including:

  • Import validation and blocking messages (file type restrictions, archived case conflicts, self-import restrictions).
  • Read-only case operation messages (creation errors, clear success/failure, delete restrictions).
  • Data integrity validation messages (passed, failed, tamper-blocked).
  • Export failure messages.
  • Deletion confirmation prompts and error messages.
  • Case rename failure messages.

Messages are either static string constants or functions that accept parameters (typically a case number or file name) for dynamic interpolation.

When adding or modifying case operation flows, prefer importing from this module over writing inline message strings.

Frontend Error Patterns

Route boundary pattern

Use the root error boundary for unhandled route/runtime failures.

Form/message pattern

Use the shared FormMessage component for consistent inline success/error rendering in form-based flows.

Source:

  • app/components/form/form-message.tsx
  • app/components/form/form.module.css

Toast pattern

Toast notifications are component-driven (not a global toast service API).

Source:

  • app/components/toast/toast.tsx
  • app/routes/striae/striae.tsx

Typical async pattern

setLoading(true);
setError('');

try {
  const response = await fetch(url, options);
  if (!response.ok) {
    throw new Error('Request failed');
  }
  // success handling
} catch (error) {
  setError(error instanceof Error ? error.message : 'Operation failed');
} finally {
  setLoading(false);
}

Auth-flow examples

The following flows apply the centralized auth mapping plus local UI state handling:

  • app/routes/auth/login.tsx
  • app/components/auth/mfa-enrollment.tsx
  • app/components/auth/mfa-verification.tsx
  • app/components/user/mfa-phone-update.tsx
  • app/routes/auth/passwordReset.tsx
  • app/routes/auth/emailActionHandler.tsx

API Client and Data Layer Patterns

API client layer

Four API clients in app/utils/api/ wrap worker communication with consistent error throwing:

  • data-api-client.ts
  • user-api-client.ts
  • image-api-client.ts
  • pdf-api-client.ts

All clients throw on authentication failures (missing token provider, failed token retrieval, empty token) and non-ok responses. Callers handle errors via try/catch rather than inspecting return values.

Re-exported through app/utils/api/index.ts.

Data operations error handling

Case operations in app/utils/data/operations/ follow a layered validation approach:

  1. Validate user session first.
  2. Check permissions via result objects (see Error Handling Layers above).
  3. Validate inputs.
  4. Parse worker error responses with JSON-first fallback to text:
if (!response.ok) {
  let errorDetails = '';
  try {
    const errorPayload = await response.json();
    if (typeof errorPayload?.error === 'string') {
      errorDetails = errorPayload.error.trim();
    }
  } catch {
    // Ignore parse errors
  }
  const baseMessage = `Failed to fetch case data: ${response.status} ${response.statusText}`;
  throw new Error(errorDetails ? `${baseMessage} - ${errorDetails}` : baseMessage);
}

Permission checks

Permission functions in app/utils/data/permissions.ts never throw. They return structured objects:

{ allowed: boolean; reason?: string }

On internal errors, permissions fail closed and return a generic reason string. Callers branch on the allowed field.

Worker and API Error Patterns

Current worker response behavior

Workers use status-based handling with endpoint-specific bodies.

Common statuses in current implementations:

  • 400 for validation/request-shape issues.
  • 403 for failed worker authentication.
  • 404 for missing resources or unsupported paths.
  • 405 for unsupported methods.
  • 500 for unexpected failures.
  • 502 for worker configuration errors (PDF worker, MissingSecretError).
  • 504 for browser/timeout errors (PDF worker).

Most workers now use JSON { error: string } response bodies:

  • Audit worker: JSON via shared createWorkerResponse helper.
  • Data worker: JSON via shared createWorkerResponse helper.
  • Image worker: JSON via createJsonResponse helper.
  • PDF worker: JSON via jsonResponse helper, with specific error types for configuration (502) and timeout (504) failures.
  • User worker: Plain-text error bodies (only remaining outlier).

App-side handling implications

Most callers can parse error responses as JSON. The recommended defensive pattern remains JSON-first with text fallback:

if (!response.ok) {
  let errorDetails = '';
  try {
    const errorPayload = await response.json();
    if (typeof errorPayload?.error === 'string') {
      errorDetails = errorPayload.error.trim();
    }
  } catch {
    errorDetails = await response.text().catch(() => '');
  }
  throw new Error(errorDetails || `Request failed with status ${response.status}`);
}

Audit Logging Error Behavior

Many workflows log audit events but do not block primary actions if audit logging fails.

Examples include login, MFA, password reset, file operations, and PDF generation.

Pattern used:

  • Execute core operation.
  • Attempt audit log in nested try/catch.
  • On audit failure, log to console and continue.

This is intentional fail-open behavior for observability side effects.

Common Scenarios

Authentication failure

  • Catch Firebase error.
  • Convert with handleAuthError.
  • Show user-safe message.
  • Optionally log security-violation audit event.

Worker request failure

  • Check response.ok.
  • Parse body safely (JSON or text).
  • Throw or return a user-safe error string.

Input correction loop

  • Show validation message.
  • Clear message when user edits related input.
  • Re-validate on submit.

Route/runtime exception

  • Root ErrorBoundary renders fallback UI.

Current Gaps and Hardening Opportunities

Known opportunities based on current codebase:

  • User worker still returns plain-text error bodies while other workers have standardized on JSON.
  • Some worker 500 responses may include raw error message text.
  • Error telemetry integration is not centralized in production code.

Developer Checklist

When adding or modifying features:

  • Use handleAuthError/getValidationError for Firebase auth UI messages.
  • Use case-messages constants from app/utils/ui/case-messages.ts for case operation text.
  • Use API clients from app/utils/api/ rather than writing raw fetch calls to workers.
  • Follow the permission result-object pattern (check allowed, read reason) instead of try/catch for permission checks.
  • Clear stale errors before new async requests.
  • Check response.ok for all fetch calls.
  • Parse failure bodies defensively (JSON with text fallback).
  • Keep user-facing messages actionable and non-sensitive.
  • Use shared UI components for messaging where available (FormMessage, Toast).
  • Treat audit logging as best-effort unless the feature explicitly requires hard-fail behavior.

Related Documentation