Repository navigation
Description
Summary
sendDesktopNotification ends in an unguarded new window.Notification(...) (desktop/src/features/notifications/lib/desktop.ts:383). Every caller discards the returned promise without a rejection handler, so a throwing notification constructor becomes an unhandled promise rejection: the notification is silently dropped and the only trace is console noise.
This is pre-existing on main. Found while measuring throw-resilience for #4900 and ruled non-blocking for that PR — the blast radius is one dropped notification, and the next notification delivers normally.
Measured
Rig: the mounted-hook harness with window.Notification replaced mid-run by a constructor that throws, then restored to drive a second notification.
ARM2_RESULT syncThrow=none unhandledRejections=1 rejectionName=NotificationError
siblingRenders=1 alertsAfterRecovery=1
So: no render crash (the throw is on an async path, not in render), one unhandled rejection, the tree stays alive, and recovery is automatic — the next notification delivers.
The realistic trigger is a notification backend that becomes unavailable after permission was granted.
Affected call sites (all on main)
| site | shape | exposed |
|---|---|---|
app/useAppShellDesktopNotifications.ts:79 |
void sendDesktopNotification(...).then(...) |
yes — .then with no .catch |
app/useAppShellDesktopNotifications.ts:127 |
void sendDesktopNotification(...).then(...) |
yes — same |
features/reminders/useReminderNotifications.ts:108 |
void sendDesktopNotification(...).then(...) |
yes — same |
features/notifications/use-feed-desktop-notifications.ts:116 |
await inside an async useEffectEvent |
yes — the event is invoked as void deliverFeedNotification(...) at :220, so the rejection escapes there instead |
All four are exposed. The fourth is worth calling out because it looks different — it uses await rather than a floating .then — but the await sits inside an async function that is itself called with a bare void, so the rejection surfaces identically one level up. A .then-vs-await audit that stops at the call to sendDesktopNotification would classify it as safe.
#4900 adds a fifth caller with the same shape; it does not introduce the pattern, and lib/desktop.ts is untouched by it (git diff origin/main...2b5efb94 -- desktop/src/features/notifications/lib/desktop.ts is empty).
Suggested fix
Guard inside sendDesktopNotification rather than at four (soon five) call sites: wrap the constructor and return false on throw. That matches the function's existing contract — it already returns false for "did not send" when permission is not granted, so callers that check didSend need no change, and the failure stops being invisible.
A .catch at each call site also works but leaves the next caller to remember.
Testing note
The regression test needs to assert on the rejection, not on the notification count — a dropped notification and a never-attempted notification produce the same count. Registering a process.on("unhandledRejection", ...) handler is what distinguishes them.
Acceptance (ported from the duplicate #5080, filed independently by Max)
- A throwing Notification constructor causes no
unhandledrejectionevent. - The failed notification does not poison later notification delivery.
- Existing successful notification behavior is unchanged.
Activity
fix(desktop): drop unhandled rejection from throwing window.Notificat…
fix(desktop): drop unhandled rejection from throwing window.Notificat…
fix(desktop): drop unhandled rejection from throwing window.Notificat…
fix(desktop): drop unhandled rejection from throwing window.Notificat…
fix(desktop): drop unhandled rejection from throwing window.Notificat…