Fix unhandled rejection from session ping when network is unavailable #56

Merged
chrisp merged 1 commit from fix/session-ping-unhandled-rejection into main 2026-05-27 16:14:15 +00:00
Owner

Summary

  • The 15-minute session keep-alive interval in useDraftAuthRecovery had no try-catch. When authClient.getSession() is called while the network is down (e.g. a laptop waking from sleep), better-fetch throws a TypeError: Failed to fetch — producing an unhandled promise rejection that was reported in Sentry.
  • Fixed by wrapping the interval callback in a try-catch. Network-level TypeErrors are silently swallowed because real session expiry (HTTP 401) is returned in-band as { data: null } by better-fetch and already caught by the !session check.
  • Improved the comment to explain the mechanism rather than citing socket offline detection (which doesn't validate auth).
  • Added 3 tests covering: network throw (no authDegraded), null session response (authDegraded set), valid session (no change).

Root cause

User left the draft page open overnight. The 15-minute ping fired repeatedly. At some point the computer woke from sleep with the network not yet ready — getSession() threw, setInterval dropped the rejected promise, Sentry caught it as an unhandled rejection. Confirmed via Sentry breadcrumbs: last activity May 26 8:32 PM, error fired May 27 12:23 PM (~28 hours later).

Test plan

  • 3 new tests pass
  • typecheck clean
  • After deploy: monitor Sentry for recurrence

🤖 Generated with Claude Code

## Summary - The 15-minute session keep-alive interval in `useDraftAuthRecovery` had no `try-catch`. When `authClient.getSession()` is called while the network is down (e.g. a laptop waking from sleep), `better-fetch` throws a `TypeError: Failed to fetch` — producing an unhandled promise rejection that was reported in Sentry. - Fixed by wrapping the interval callback in a `try-catch`. Network-level `TypeError`s are silently swallowed because real session expiry (HTTP 401) is returned in-band as `{ data: null }` by `better-fetch` and already caught by the `!session` check. - Improved the comment to explain the mechanism rather than citing socket offline detection (which doesn't validate auth). - Added 3 tests covering: network throw (no authDegraded), null session response (authDegraded set), valid session (no change). ## Root cause User left the draft page open overnight. The 15-minute ping fired repeatedly. At some point the computer woke from sleep with the network not yet ready — `getSession()` threw, `setInterval` dropped the rejected promise, Sentry caught it as an unhandled rejection. Confirmed via Sentry breadcrumbs: last activity May 26 8:32 PM, error fired May 27 12:23 PM (~28 hours later). ## Test plan - [x] 3 new tests pass - [x] typecheck clean - [ ] After deploy: monitor Sentry for recurrence 🤖 Generated with [Claude Code](https://claude.com/claude-code)
chrisp added 1 commit 2026-05-27 16:09:22 +00:00
Fix unhandled rejection from session ping when network is unavailable
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m36s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m21s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 50s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
43660d16e9
The 15-minute keep-alive interval in useDraftAuthRecovery called
authClient.getSession() with no try-catch. When the network is
unavailable (e.g. computer waking from sleep), better-fetch throws a
TypeError rather than returning an in-band error, producing an
unhandled promise rejection that was surfacing in Sentry.

Adds a try-catch that silently swallows network TypeErrors — real
session expiry (401) is still caught by the existing !session check
because better-fetch returns HTTP errors in-band as { data: null }.

Adds tests covering the three ping outcomes: network throw, null
session (expired), and valid session.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
chrisp merged commit 8a40953019 into main 2026-05-27 16:14:15 +00:00
Sign in to join this conversation.
No description provided.