fix(ui): Surface fetch errors instead of swallowing them#1168
Conversation
Client-side fetch and submit handlers caught errors silently, leaving users with hung spinners, blank pages, or misleading empty states. Add handleClientError(err, context) in src/utils/ that logs with context and returns a user-safe message, and use it at every fixed site. Components now set an error state and render the message with a retry affordance (retry button or the original action button). Fixed sites: - challenges/index: recommended + history fetches (per-panel error and retry), hint and solution handlers (shared error banner) - challenges/[id]: hint, coach, and solution handlers - containers/profile/bio: profile load (was an infinite spinner) and profile save - components/profile/TroopDashboard: module-change PATCH - lessons/lesson/[id]: lesson load (was a blank page), with retry - community/index: candidates load, with retry Non-ok responses in these paths now throw and surface too, instead of being ignored. Deliberate non-critical suppressions (safe-storage, analytics, optional decorative panels, hooks that track their own errors) are left alone.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR improves client-side UX and debuggability by surfacing fetch/submit failures (instead of silently swallowing them), using a shared handleClientError helper that logs context and returns a renderable message.
Changes:
- Added
handleClientError(error, context)utility and unit tests. - Updated multiple pages/components to throw on non-OK responses, set error state, and render inline error + Retry affordances.
- Expanded challenges page tests to cover “reject/non-ok surfaces error” and “Retry recovers” flows.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/utils/handle-client-error.ts |
New helper for contextual logging + returning a message for UI error state. |
src/utils/__tests__/handle-client-error.test.ts |
Unit tests validating helper logging and message selection. |
src/pages/lessons/lesson/[id].tsx |
Adds load error state + Retry for lesson fetch (throws on non-OK). |
src/pages/community/index.tsx |
Adds load error state + Retry for candidates fetch (but currently only catches network errors). |
src/pages/challenges/index.tsx |
Surfaces errors for recommended/history fetches and hint/solution actions; adds Retry UI for panels. |
src/pages/challenges/[id].tsx |
Surfaces errors for hint/coach/solution actions using existing error banner. |
src/containers/profile/bio.tsx |
Surfaces profile load/save errors with Retry (load) and inline save error. |
src/components/profile/TroopDashboard.tsx |
Surfaces module update failures inline under the module select. |
__tests__/pages/challenges/index.test.tsx |
Adds coverage for surfaced fetch errors and Retry recovery behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ch-blocks # Conflicts: # src/components/profile/RepositoryShowcase.tsx
|
@copilot resolve the merge conflicts in this pull request |
I checked this branch locally and there are no remaining merge conflicts to resolve: |
Problem
Client-side fetch/submit handlers across the app caught errors silently (
catch { /* non-critical */ }). When a request failed, users saw hung spinners (profile bio loaded forever), blank pages (lesson detail), misleading empty states ("No challenges attempted yet" on a 500), or buttons that simply did nothing (Get Hint, Reveal Solution, coach, module change, profile save).Solution
handleClientError(err, context)insrc/utils/handle-client-error.ts: logs with context viaconsole.errorand returns a user-safe message string.package.json, so per-component error state was used (option 2 from the issue). No dependencies added.Fixed sites:
src/pages/challenges/index.tsxfetchRecommended,fetchHistorysrc/pages/challenges/index.tsxhandleGetHint,handleShowSolutionsrc/pages/challenges/[id].tsxhandleGetHint,handleCoach,handleShowSolutionsrc/containers/profile/bio.tsxfetchProfile,handleSavesrc/components/profile/TroopDashboard.tsxhandleModuleChangesrc/pages/lessons/lesson/[id].tsxsrc/pages/community/index.tsxSkipped (deliberate non-critical suppressions, one line each):
src/utils/safe-storage.ts(4 sites) — storage availability/cleanup guards, silent by design.src/lib/translator-analytics.ts— analytics must never break the app.src/components/translator/TranslatorForm.tsx(3) — auto-fill conveniences degrade to manual entry; PDF upload errors are surfaced via theusePdfUploadhook's own error state.src/components/jobs/ResumeScorer.tsx— same:usePdfUploadtracks and renders the error.src/components/profile/TroopDashboard.tsxfetchWarmups/fetchProgress/fetchXp/fetchStreak/fetchToday— decorative tiles/trays that hide gracefully; the primary dashboard fetch already sets an error.src/components/profile/ProfileSettings.tsx— optional placements/MOS panels hide; the destructive actions already set a message state.src/components/ai-assistant/AITeachingAssistant.tsxsendFeedback— optimistic thumbs-up/down, non-critical.src/pages/challenges/browse.tsxfetchTopics— topic chips stay empty; the catalog fetch already surfaces errors.src/pages/lessons/index.tsxrecall fetch — optional recall tray hides; the module grid is static.src/pages/admin/j0di3/index.tsxstats fetch — admin convenience tiles hide; nav links are static.src/pages/profile/[id].tsx,src/pages/p/[callsign].tsx— server-sidegetServerSidePropsfallbacks (avatar fallback /notFound), not client-side.Biome rule:
suspicious/noEmptyBlockStatementsis already enabled aterrorinbiome.json(with test/scripts overrides). It permits comment-only blocks by design, which is why thesecatch { // non-critical }blocks passed lint — no config change needed.Note: staged files were normalized by the pre-commit
biome check --writehook, which accounts for the formatting-only hunks in__tests__/pages/challenges/index.test.tsx.Verification
npm run typecheck— no new errors (7 pre-existing errors in untouched__tests__/{lib,pages/api}/j0di3/*files exist on master)npm run lint— changed files contribute 0 errors; repo baseline unchanged (25 pre-existing errors on master)npm test— 390 passed (42 files), including 3 new challenges-page tests (rejecting fetch shows error + Retry recovers; non-ok history surfaces; hint failure surfaces) and 3 helper unit testsnpm run build— succeeds (exit 0)Review updates
src/pages/community/index.tsx: theif (res.ok)guards still skipped state on a 4xx/5xx, leaving empty sections despite being listed as fixed (caught in review). Hardened to throw on any non-OK response so the existing error + Retry path surfaces it.src/for the same pattern (if (res.ok)with noelse,.catch(() => …)): no remaining genuine silent failures. The other ~25.catch/res.oksites are the documented best-effort suppressions already listed under "Skipped" (optional enrichment tiles, prefetch/warmup,safe-storage, telemetry,navigator.sharecancellation).Closes #1058