siyuan-note/siyuan · error

Recorder has been disposed

Error message

Recorder has been disposed

What it means

Thrown by RecordMedia.startRecording when the recorder instance has already been disposed. Once dispose() runs, its resources (AudioContext, tracks) are released, so starting a new recording on the dead instance is refused instead of failing obscurely later. A new RecordMedia instance must be created for the next recording.

Solutions

  1. Guard calls: check the instance is not disposed (or track disposal in your wrapper) before calling startRecording
  2. Create a fresh RecordMedia instance for each recording session instead of reusing a disposed one
  3. Remove/nullify references and event handlers to the recorder when disposing so stale callers cannot reach it
  4. Catch the error and restart the flow with a newly constructed recorder

Example fix

// before
await cachedRecorder.startRecording();
// after
if (cachedRecorder?.disposed) { cachedRecorder = new RecordMedia(stream); }
await cachedRecorder.startRecording();
Defensive patterns

Strategy: try-catch

Validate before calling

// caller-side bookkeeping
let disposed = false;
const isAlive = () => recorder != null && !disposed;

Try / catch

try {
  await recorder.startRecording();
} catch (e) {
  if (e.message === "Recorder has been disposed") {
    recorder = new RecordMedia(stream); // restart with a fresh instance
    await recorder.startRecording();
  } else throw e;
}

Prevention

When it happens

Trigger: Calling await recorder.startRecording() after recorder.dispose() has been called — e.g. a stale UI handler or a queued promise resolving after the recording dialog was closed.

Common situations: User closes the recording dialog while a start action is in flight; components caching a recorder instance across sessions; event listeners not removed on teardown firing after dispose.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/de978832e38328dc. Report an issue: GitHub.

Appendix: source

Thrown at app/src/protyle/util/RecordMedia.ts:114

        }

        const contextOptions = {} as AudioContextOptions & { sinkId?: { type: "none" } };
        if ((isInAndroid() || isInHarmony()) && "setSinkId" in AudioContextConstructor.prototype) {
            contextOptions.sinkId = {type: "none"};
        }
        this.context = new AudioContextConstructor(contextOptions);
        if (!this.context.audioWorklet || typeof AudioWorkletNode === "undefined") {
            throw new Error("AudioWorklet is not supported");
        }
        this.audioInput = this.context.createMediaStreamSource(mediaStream);
        this.mediaStream.getAudioTracks().forEach((track) => {
            track.addEventListener("ended", this.handleTrackEnded);
        });
    }

    public async startRecording() {
        if (this.disposed) {
            throw new Error("Recorder has been disposed");
        }
        if (this.isRecording) {
            return;
        }

        this.chunks = [];
        this.readyPromise = new Promise<void>((resolve, reject) => {
            this.resolveReady = resolve;
            this.rejectReady = reject;
        });
        // Webpack 通过 import.meta.url 将录音编码器打包为按需加载的独立 Worker。
        // @ts-ignore TypeScript 的 CommonJS 类型检查不识别由 Webpack 转换的 import.meta.url。
        this.worker = new Worker(new URL("./RecordMediaWorker.ts", import.meta.url));
        this.worker.onmessage = (event: MessageEvent<EncoderMessage>) => {
            this.handleWorkerMessage(event.data);
        };
        this.worker.onerror = (event: ErrorEvent) => {
            this.handleWorkerError(new Error(event.message || "MP3 encoder failed"));

View on GitHub (pinned to 8641553a1f)