{"record":{"id":"c43fccb263348ecd","repo":"block/buzz","slug":"audio-decode-cancelled","errorCode":null,"errorMessage":"Audio decode cancelled","messagePattern":"Audio decode cancelled","errorType":"exception","errorClass":"DOMException","httpStatus":null,"severity":"info","filePath":"desktop/src/features/messages/ui/AudioMessageAttachment.tsx","lineNumber":82,"sourceCode":"  if (pathname.endsWith(\".mp4\")) return \"audio/mp4\";\n  if (pathname.endsWith(\".mp3\")) return \"audio/mpeg\";\n  if (pathname.endsWith(\".ogg\")) return \"audio/ogg\";\n  return \"audio/wav\";\n}\n\nfunction isAbortError(error: unknown): boolean {\n  return error instanceof DOMException && error.name === \"AbortError\";\n}\n\nasync function decodeSamples(\n  url: string,\n  signal: AbortSignal,\n): Promise<Float32Array> {\n  const response = await fetch(url, { signal });\n  if (!response.ok) throw new Error(`Audio fetch failed (${response.status})`);\n  const bytes = await response.arrayBuffer();\n  if (signal.aborted) {\n    throw new DOMException(\"Audio decode cancelled\", \"AbortError\");\n  }\n  const context = new AudioContext();\n  let rejectCancellation: ((reason?: unknown) => void) | undefined;\n  const cancellation = new Promise<never>((_resolve, reject) => {\n    rejectCancellation = reject;\n  });\n  const onAbort = () => {\n    void context.close().catch(() => undefined);\n    rejectCancellation?.(\n      new DOMException(\"Audio decode cancelled\", \"AbortError\"),\n    );\n  };\n  signal.addEventListener(\"abort\", onAbort, { once: true });\n  try {\n    const buffer = await Promise.race([\n      context.decodeAudioData(bytes),\n      cancellation,\n    ]);","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/block/buzz/blob/dad5a33865fc81a2e55b3b60746632f615ec1e3a/desktop/src/features/messages/ui/AudioMessageAttachment.tsx#L64-L100","documentation":"After fetching the audio bytes, decodeSamples checks signal.aborted before starting AudioContext decoding and throws AbortError 'Audio decode cancelled'. This prevents wasted CPU decoding audio for a consumer that has already lost interest.","triggerScenarios":"The AbortSignal fires (component unmount, message scrolled away, attachment recycled) between response.arrayBuffer() completing and the decode step — i.e. abort landed during the arrayBuffer await.","commonSituations":"User scrolls fast through a voice-note-heavy channel; message list virtualization unmounts the audio attachment mid-download; channel switch aborts pending loads.","solutions":["Check signal.aborted after each await in the caller and bail early","Catch AbortError in load() and ignore it — it is expected cancellation, not failure","Avoid recreating attachments rapidly (stabilize list item identity) so aborts fire less often"],"exampleFix":"// before\nconst bytes = await response.arrayBuffer();\n// (abort here silently reached decode)\n// after\nconst bytes = await response.arrayBuffer();\nif (signal.aborted) throw new DOMException(\"Audio decode cancelled\", \"AbortError\");","handlingStrategy":"try-catch","validationCode":"if (signal.aborted) return; // don't start decode work at all","typeGuard":"function isAbortError(e: unknown): e is DOMException {\n  return e instanceof DOMException && e.name === 'AbortError';\n}","tryCatchPattern":"try {\n  const samples = await decodeSamples(url, signal);\n} catch (e) {\n  if (isAbortError(e)) return; // cancelled, do not render error state\n  throw e;\n}","preventionTips":["Check signal.aborted after every awaited step in long async chains","Keep AbortController creation scoped to the mounted component/effect","Treat post-fetch abort checks as required hygiene in decode pipelines"],"tags":["abort","cancellation","audio","decode"],"backgroundTag":"abort-error-cancellation","analyzedSha":"dad5a33865fc81a2e55b3b60746632f615ec1e3a","analyzedAt":"2026-09-05T18:13:50.666Z","contentChangedAt":"2026-09-05T18:13:50.666Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}