{"record":{"id":"3cd5130f1391f0db","repo":"ChatGPTNextWeb/NextChat","slug":"recording-not-started","errorCode":null,"errorMessage":"Recording not started","messagePattern":"Recording not started","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"app/lib/audio.ts","lineNumber":88,"sourceCode":"          // @ts-ignore\n          this.recordBuffer.push.apply(this.recordBuffer, int16Data);\n        }\n      };\n\n      this.source.connect(this.workletNode);\n      this.source.connect(this.mergeNode, 0, 0);\n      this.workletNode.connect(this.context.destination);\n\n      this.workletNode.port.postMessage({ command: \"START_RECORDING\" });\n    } catch (error) {\n      console.error(\"Error starting recording:\", error);\n      throw error;\n    }\n  }\n\n  stopRecording() {\n    if (!this.workletNode || !this.source || !this.stream) {\n      throw new Error(\"Recording not started\");\n    }\n\n    this.workletNode.port.postMessage({ command: \"STOP_RECORDING\" });\n\n    this.workletNode.disconnect();\n    this.source.disconnect();\n    this.stream.getTracks().forEach((track) => track.stop());\n  }\n  startStreamingPlayback() {\n    this.isPlaying = true;\n    this.nextPlayTime = this.context.currentTime;\n  }\n\n  stopStreamingPlayback() {\n    this.isPlaying = false;\n    this.playbackQueue.forEach((source) => source.stop());\n    this.playbackQueue = [];\n    this.playBuffer = [];","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/ChatGPTNextWeb/NextChat/blob/defdcdb55d850cd12c4c657eb83729fd66e215c0/app/lib/audio.ts#L70-L106","documentation":"Thrown at app/lib/audio.ts:88 inside AudioHandler.stopRecording() when any of workletNode, source, or stream is null — i.e. startRecording() never completed successfully. stopRecording assumes the recording graph is wired; calling it before startRecording (or after a failed startRecording that threw and left the fields null) violates that precondition.","triggerScenarios":"Calling stopRecording() before startRecording(); startRecording() threw inside getUserMedia/createMediaStreamSource/addModule (mic permission denied, worklet module 404, AudioContext suspended) leaving workletNode null; calling stopRecording() twice (the second call sees fields nulled after disconnect).","commonSituations":"User clicks stop before clicking record; mic permission was denied so startRecording failed silently in a caller that swallowed the error; audio-processor.js failed to load; AudioContext is suspended on iOS/Safari and startRecording did not resume; double-stop from a race in the UI.","solutions":["Guard stopRecording with an early return when not recording instead of throwing.","Track an explicit isRecording boolean and only call stopRecording when true.","Ensure startRecording's error is surfaced (not swallowed) so the UI stays in sync and stop is not invoked.","Disable the stop button until startRecording resolves successfully."],"exampleFix":"// before\nstopRecording() {\n  if (!this.workletNode || !this.source || !this.stream) {\n    throw new Error(\"Recording not started\");\n  }\n  // ...\n}\n\n// after\nstopRecording() {\n  if (!this.workletNode || !this.source || !this.stream) {\n    return; // nothing to stop, no-op\n  }\n  this.workletNode.port.postMessage({ command: \"STOP_RECORDING\" });\n  this.workletNode.disconnect();\n  this.source.disconnect();\n  this.stream.getTracks().forEach((track) => track.stop());\n  this.workletNode = null;\n  this.source = null;\n  this.stream = null;\n}","handlingStrategy":"validation","validationCode":"class AudioHandler {\n  private isRecording = false;\n\n  async startRecording(onChunk: (c: Uint8Array) => void) {\n    // ... existing setup ...\n    this.isRecording = true;\n  }\n\n  canStop(): boolean {\n    return this.isRecording && !!this.workletNode && !!this.source && !!this.stream;\n  }\n}\n\nif (handler.canStop()) {\n  handler.stopRecording();\n}","typeGuard":"function isRecordingGraphReady(h: AudioHandler): boolean {\n  // Access via public getter if added; otherwise guard from the caller using an isRecording flag\n  return (h as unknown as { workletNode: unknown; source: unknown; stream: unknown })\n    .workletNode != null;\n}","tryCatchPattern":"try {\n  handler.stopRecording();\n} catch (e) {\n  if (e instanceof Error && /not started/.test(e.message)) {\n    // no recording in progress — safe to ignore\n  } else {\n    throw e;\n  }\n}","preventionTips":["Track an explicit isRecording flag and gate stopRecording on it.","Surface startRecording errors to the UI so stop is never called on a failed start.","Make stopRecording idempotent (no-op when not recording) instead of throwing.","Null out workletNode/source/stream after stopping so a second stop is a clean no-op."],"tags":["audio","web-audio","recording","lifecycle","precondition"],"backgroundTag":null,"analyzedSha":"defdcdb55d850cd12c4c657eb83729fd66e215c0","analyzedAt":"2026-08-12T09:57:26.489Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}