Skip to content

fix(announcer): swallow benign speech interruptions#36

Merged
chiefcll merged 1 commit into
mainfrom
fix/announcer-speech-interrupted
Jun 22, 2026
Merged

fix(announcer): swallow benign speech interruptions#36
chiefcll merged 1 commit into
mainfrom
fix/announcer-speech-interrupted

Conversation

@chiefcll

Copy link
Copy Markdown
Contributor

What

Error: Speech synthesis error: interrupted was spamming the console during directional navigation. This makes the announcer correctly treat speech interrupted/canceled as the benign, expected events they are.

Why

The error originates in speak()'s utterance.onerror (speech.ts). interrupted/canceled fire on every normal announcement, because each new announcement calls synth.cancel() to replace the in-flight one.

speakSeries already intended to swallow these, but the swallow branch was gated on e instanceof SpeechSynthesisErrorEvent — while speak() rejected a plain Error. So:

  • the instanceof check was always false → the error hit else { throw e },
  • which rejected the series promise,
  • which, on the non-notification announce path (never .catched), surfaced as an unhandled rejection in the console.

The same dead branch also defeated the network retry logic, and SpeechSynthesisErrorEvent isn't defined as a global on every TV browser (a ReferenceError waiting to happen if the branch ever evaluated).

Changes

  • speak() now rejects an Error that carries the structured .error code.
  • Added a single handleSpeechError() classifier (replacing two copy-pasted catch blocks) that classifies by the .error code string:
    • interrupted/canceled → stop retrying silently (no longer surfaced),
    • network → back off and retry (this path was also dead before),
    • anything unexpected → rethrow.
  • Dropped the dependency on the SpeechSynthesisErrorEvent global.

Reviewer notes

  • A genuinely unexpected speech error (e.g. synthesis-failed) on the non-notification path still has no .catch in announcer.ts, so it would be an unhandled rejection. That's now rare and arguably should be visible — happy to add a .catch safety net on the returned series if preferred.

Verification

  • npm run tsc — clean
  • npm run lint — 0 errors (speech.ts introduces no new warnings)
  • npm run test — 124/124 pass

🤖 Generated with Claude Code

Speech synthesis "interrupted"/"canceled" errors fire on every normal
announcement (each new announcement calls synth.cancel() to replace the
previous one), but they were leaking to the console as
"Error: Speech synthesis error: interrupted".

speakSeries intended to swallow these, but gated the swallow branch on
`e instanceof SpeechSynthesisErrorEvent` while speak() rejected a plain
Error — so the check never matched, the error hit `throw e`, rejected the
series promise, and surfaced as an unhandled rejection on the
non-notification announce path. The same dead branch also defeated the
network retry logic.

- speak() now rejects an Error carrying the structured .error code.
- Add a single handleSpeechError() classifier (replacing two copy-pasted
  catch blocks) that classifies by the .error code: interrupted/canceled
  stop retrying silently, network backs off and retries, anything else
  rethrows.
- Drop the dependency on the SpeechSynthesisErrorEvent global, which isn't
  defined on every TV browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chiefcll chiefcll merged commit ec7d82e into main Jun 22, 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.

1 participant