Skip to content

refactor(contracts): add error boundary with retry#697

Merged
mikewheeleer merged 1 commit into
Talenttrust:mainfrom
Ade-Pheebs:refactor/contracts-11-error-boundary
Jul 25, 2026
Merged

refactor(contracts): add error boundary with retry#697
mikewheeleer merged 1 commit into
Talenttrust:mainfrom
Ade-Pheebs:refactor/contracts-11-error-boundary

Conversation

@Ade-Pheebs

@Ade-Pheebs Ade-Pheebs commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #615

An unexpected render error in the contracts section previously blanked the entire page with no recovery path for the user. This PR wraps the contracts section in a dedicated ContractsErrorBoundary that catches render failures, surfaces an accessible fallback with a Retry button, and forwards every captured error through the shared reportError seam so nothing is swallowed silently.


Problem

When any component inside the contracts page tree threw during rendering, React unmounted the whole page and left users with a blank screen and no way to recover. There was no error boundary specific to the contracts section, and no accessible notification to inform assistive-technology users that something had gone wrong.


Solution

1. New component — src/components/ContractsErrorBoundary.tsx

A React class component implementing the error boundary contract:

Lifecycle / method Behaviour
getDerivedStateFromError(error) Captures the thrown error into { hasError: true, error } state so the next render shows the fallback.
componentDidCatch(error, info) Calls reportError(error, 'ContractsErrorBoundary', 'error', { componentStack }) — routes through the existing pluggable errorReporter.ts seam (same pattern as SafeBoundary, error.tsx, and global-error.tsx). Errors are never swallowed silently.
reset() Resets hasError and error to false/null so clicking Retry re-mounts the children.
render() Returns children when healthy; returns the custom fallback prop if provided; otherwise renders the default accessible fallback.

Default fallback UI

  • Container carries role="alert" and aria-live="assertive" so screen readers announce the failure immediately.
  • Primary message: "The contracts section failed to load."
  • Secondary message: "An unexpected error occurred. You can retry or go back to the home page."
  • Retry button calls this.reset() — resets boundary state so the child tree re-mounts on the next render cycle.
  • Go Home link (href="/") gives users a non-destructive escape route.
  • All interactive elements have visible focus rings and meet WCAG 2.1 AA contrast requirements.

Custom fallback prop
Callers can pass fallback={<MyFallback />} to replace the default UI entirely (e.g. for page-level boundaries that want a full-screen error state).

2. Integration — src/app/contracts/page.tsx

The ContractsPage return value is now wrapped in <ContractsErrorBoundary>:

return (
  <ContractsErrorBoundary>
    <main className="min-h-screen p-8">
      {/* ... existing contracts UI ... */}
    </main>
  </ContractsErrorBoundary>
);

This guards the empty-state path, the contract-list path, and the ContractCreationForm modal path with a single boundary. Any render failure in any of those sub-trees will now be caught and presented gracefully.

3. Error reporting integration

ContractsErrorBoundary uses the exact same reportError seam documented in docs/error-reporting.md and already used by:

  • src/components/SafeBoundary.tsxreportError(error, 'SafeBoundary')
  • src/app/error.tsxreportError(error, 'Error Boundary')
  • src/app/global-error.tsxreportError(error, 'Global Error Boundary')

This boundary extends the pattern with severity and metadata:

reportError(error, 'ContractsErrorBoundary', 'error', {
  componentStack: info.componentStack ?? undefined,
});

The componentStack field lets any injected reporter (Sentry, Rollbar, etc.) pinpoint the exact component that threw without any additional instrumentation.


Tests — src/components/ContractsErrorBoundary.test.tsx

22 test cases, 100% statement / 100% function / 100% line coverage on ContractsErrorBoundary.tsx.

PASS src/components/ContractsErrorBoundary.test.tsx
  ContractsErrorBoundary
    normal render (no error)
      ✓ renders children when no error occurs
      ✓ does not render the fallback UI when children render successfully
    fallback UI when a child throws
      ✓ shows the default fallback message
      ✓ shows the secondary help text
      ✓ renders a Retry button
      ✓ renders a Go Home link pointing to "/"
      ✓ does not render children when in error state
    fallback accessibility
      ✓ fallback container has role="alert"
      ✓ fallback container has aria-live="assertive"
    custom fallback prop
      ✓ renders the custom fallback when provided and a child throws
      ✓ does not render the default fallback when a custom fallback is provided
      ✓ does not render the custom fallback when no error occurs
    retry behaviour
      ✓ re-renders children after clicking Retry when the underlying issue is resolved
      ✓ shows the fallback again if the child throws again after retry
    error logging
      ✓ calls console.error in non-production when a child throws
      ✓ does not swallow the error silently
    pluggable error reporter
      ✓ invokes the injected reporter with the thrown error
      ✓ passes componentStack metadata to the reporter
      ✓ uses context label "ContractsErrorBoundary"
      ✓ reports with severity level "error"
      ✓ does not call the reporter when children render without error
    multiple independent boundaries
      ✓ a throwing child in one boundary does not affect a sibling boundary

Tests:    22 passed, 22 total
Coverage: 100% Stmts | 100% Funcs | 100% Lines

Edge cases covered:

  • Child throws → fallback shown, children unmounted
  • Retry → children re-mounted if root cause resolved
  • Retry → fallback re-shown if child still throws
  • Custom fallback prop → default UI suppressed
  • Normal render → reporter never called, no fallback rendered
  • Two independent boundaries → one erroring does not affect the other
  • Pluggable reporter receives correct (error, context, level, meta) signature

Full test output

Test Suites: 72 passed (plus 1 pre-existing failure in milestones filter test unrelated to this PR)
Tests:       1236 passed, 4 failed (pre-existing)

The 4 pre-existing failures are in src/app/milestones/__tests__/page.test.tsx — confirmed present on the baseline branch (55ac248) before any changes from this PR were applied.


Pre-flight checklist

  • npm run lint — clean, exit 0
  • npm test — 1236 / 1240 pass; 4 failures are pre-existing and unrelated to this PR
  • npm run build — pre-existing environment failure (@tailwindcss/oxide native binding absent in this codespace); confirmed identical error on the baseline before this PR; TypeScript tsc --noEmit is clean
  • No merge conflicts — rebased directly onto upstream/main (b38990a)
  • 95%+ test coverage for impacted module — 100% achieved
  • Accessibility: role="alert", aria-live="assertive", visible focus rings on all interactive elements
  • Security: no secrets, no new dependencies, no external network calls introduced
  • Error not swallowed: every thrown error forwarded via reportError with context label and componentStack metadata

Files changed

File Change
src/components/ContractsErrorBoundary.tsx New — error boundary component (105 lines)
src/components/ContractsErrorBoundary.test.tsx New — 22-case test suite (403 lines)
src/app/contracts/page.tsx Modified — import + wrapper around page return

Related

Closes #615

Wraps the contracts section in a dedicated ContractsErrorBoundary so
that an unexpected render error shows an accessible fallback with a
Retry button instead of blanking the page.

- Add ContractsErrorBoundary class component (src/components/)
  * getDerivedStateFromError captures the thrown error into state
  * componentDidCatch forwards to reportError('ContractsErrorBoundary')
    with severity 'error' and componentStack metadata — errors are never
    swallowed silently
  * reset() clears error state so Retry re-mounts the children
  * Default fallback: role=alert, aria-live=assertive, descriptive
    message, Retry button, Go Home link
  * Accepts optional fallback prop for custom error UI

- Integrate boundary into src/app/contracts/page.tsx
  * ContractsPage return wrapped in <ContractsErrorBoundary>

- Comprehensive tests (22 cases, 100 % stmt/fn/line coverage)
  * Normal render — children pass-through, no fallback shown
  * Fallback UI content, Retry button, Go Home link href='/'
  * Accessible attributes: role=alert and aria-live=assertive
  * Custom fallback prop replaces default UI
  * Retry re-mounts children after reset; re-throws if still broken
  * Error logging via console.error (default reporter)
  * Pluggable reporter receives error, context label, severity, meta
  * componentStack metadata forwarded to reporter
  * Multiple independent boundaries do not interfere

Closes Talenttrust#615
@drips-wave

drips-wave Bot commented Jul 25, 2026

Copy link
Copy Markdown

@Ade-Pheebs Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@mikewheeleer

Copy link
Copy Markdown
Contributor

looks good @Ade-Pheebs — well scoped and easy to review. merging 👍

@mikewheeleer
mikewheeleer merged commit 2a4a00e into Talenttrust:main Jul 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wrap the contracts section in an error boundary with a retry

2 participants