Skip to content

fix: reject with proper Error on audio error event (fixes #339115) #339119

Description

@vs-code-engineering

Summary

An accessibility signal sound fails to play, and the error event handler in doPlayAudio rejects the promise with e.error — but a DOM media error event is a plain Event that has no error property, so the promise rejects with undefined. That undefined then reaches the catch (e) block in playSound, which reads e.message and throws TypeError: Cannot read properties of undefined (reading 'message'). Impact: an unhandled error is logged to telemetry whenever an accessibility cue sound fails to load/play (e.g. missing/blocked media), affecting 425 users on stable 1.140.0.

Fixes #339115
Recommended reviewer: @meganrogge

Culprit Commit

Field Value
Commit pre-existing — not a regression
Author n/a
PR n/a
Message n/a
Why The faulty reject(e.error) has existed since the file was created; no recent commit changed the triggering logic. This bucket is new on the 1.140.0 stable channel but the defect is long-standing (surfaced at stable scale / under conditions where a cue sound fails to play). This is best characterized as a pre-existing bug newly observed, not a code regression.

No culprit commit identified: the defective line predates the available
history for this file and no commit in the regression window altered the
audio error-handling path. The fix targets the long-standing producer bug.

Code Flow

sequenceDiagram
    participant Caller as playSignal
    participant Sound as playSound
    participant Audio as doPlayAudio
    participant DOM as HTMLAudioElement

    Caller->>Sound: await playSound(sound)
    Sound->>Audio: await playAudio(url, volume)
    Audio->>DOM: new Audio(url); audio.play()
    DOM-->>Audio: 'error' event (plain Event, no .error)
    Note over Audio: ⚠️ Root cause (L297):<br/>reject(e.error) → rejects with undefined
    Audio-->>Sound: promise rejects with undefined
    Note over Sound: 💥 L211: e.message<br/>Cannot read properties of<br/>undefined (reading 'message')
Loading

Affected Files

File Role Evidence
src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts root cause (producer) L297: reject(e.error) — the media error event is a plain Event with no .error property, so the promise rejects with undefined
src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts crash site L211 (from stack): if (!e.message.includes(...)) reads .message on the undefined rejection value

Repro Steps

  1. Trigger any accessibility signal that plays a sound (e.g. enable sounds via accessibility.signalOptions, then perform an action that plays a cue).
  2. Cause the audio element to emit an error event — e.g. the media resource fails to load, is blocked, or is otherwise unplayable (the browser/Electron fires error rather than resolving play()).
  3. The error handler rejects the play promise with undefined; the catch (e) in playSound reads e.message and throws TypeError: Cannot read properties of undefined (reading 'message'), which is reported as an unhandled error.

Because play()'s own rejection path (L300) rejects with a real Error, the crash only manifests via the media error event path, which explains the intermittent, environment-dependent occurrence.

How the Fix Works

Chosen approach (accessibilitySignalService.ts → doPlayAudio): fix the data producer, not the crash site. The error-event handler no longer rejects with the non-existent e.error. Instead it constructs a proper Error, enriched with the real failure details from audio.error (an HTMLMediaElement's MediaError, carrying .code and .message) when available, and a generic "Failed to play audio" message otherwise. After this change the promise always rejects with a genuine Error instance, so the downstream catch (e) in playSound can always read e.message safely — the TypeError can no longer be produced.

This keeps the existing logService/console.error telemetry path intact (the catch block still runs and still logs genuine failures), and it enriches the error with cross-boundary diagnostic context (MediaError.code) rather than swallowing it.

Alternatives considered:

  • Guard at the crash site (e?.message or if (!e) return in playSound's catch) — rejected: that is a crash-site guard that masks the producer bug, leaves every other consumer of playAudio receiving a meaningless undefined rejection, and discards the real failure cause.
  • Wrap the play call in try/catch that swallows the error — rejected: it would hide genuine audio failures from the telemetry pipeline.

Recommended Owner

@meganrogge — area owner of accessibilitySignalService.ts; authored the large majority of this file's history and is highly active in microsoft/vscode (hundreds of commits in the last 90 days). The culprit is pre-existing, so ownership falls to the feature-area owner.


Note

This was originally intended as a pull request, but PR creation failed. The changes have been pushed to the branch fix/accessibility-signal-undefined-error-339115-fd9bae4896d46f3c.

Original error: ERR_API: [2026-10-01T15:40:49.269Z] create pull request in microsoft/vscode failed (attempt 1)

Original error: Validation Failed: {"resource":"PullRequest","code":"custom","field":"fork_collab","message":"fork_collab Fork collab can't be granted by someone without permission"} - https://docs.github.com/rest/pulls/pulls#create-a-pull-request
Retryable: false
Suggestion: This error cannot be resolved by retrying. Please check the error details and fix the underlying issue.

To create the pull request manually:

gh pr create --title "fix: reject with proper Error on audio error event (fixes #339115)" --base main --head vscodebot-pr:fix/accessibility-signal-undefined-error-339115-fd9bae4896d46f3c --repo microsoft/vscode
Show patch (36 lines)
From 10fad800e35a5532e8680893099e22d61a9bf2d8 Mon Sep 17 00:00:00 2001
X-GH-AW-Base-Commit: 69ae25e227153d7795e539a54386b0c82988745f
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Thu, 1 Oct 2026 15:32:41 +0000
Subject: [PATCH] fix: reject with proper Error on audio error event (fixes
 #339115)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 .../browser/accessibilitySignalService.ts                | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts
index 3576d7b9ba9..51b5647bb90 100644
--- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts
+++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts
@@ -294,9 +294,12 @@ function doPlayAudio(url: string, volume: number, disposables: DisposableStore):
 		disposables.add(addDisposableListener(audio, 'ended', () => {
 			resolve(audio);
 		}));
-		disposables.add(addDisposableListener(audio, 'error', (e) => {
-			// When the error event fires, ended might not be called
-			reject(e.error);
+		disposables.add(addDisposableListener(audio, 'error', () => {
+			// When the error event fires, ended might not be called.
+			// The media `error` event is a plain Event without an `error`
+			// property; the actual failure is described by `audio.error`.
+			const mediaError = audio.error;
+			reject(new Error(mediaError ? `Failed to play audio (code ${mediaError.code}): ${mediaError.message}` : 'Failed to play audio'));
 		}));
 		audio.play().catch(e => {
 			// When play fails, the error event is not fired.
-- 
2.54.0

Generated by errors-fix · copilot · opus48 · 229.5 AIC · ⌖ 39.4 AIC · ⊞ 19.1K · ◷

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions