hcengineering/platform · error

Cannot stop from state: ${state}

Error message

Cannot stop from state: ${state}

What it means

`stop()` may only run while the recorder is 'recording' or 'paused'. This error is thrown when `recorder.stop()` is called from any other state ('idle', 'ready', 'stopping', 'stopped'), meaning there is no recording in progress to finalize. It exists so the recorder does not attempt to finalize a MediaRecorder that does not exist or is already being finalized.

Source

Thrown at plugins/recorder-resources/src/stores/recorder.ts:147

      const { screenStream, micStream } = get(manager)
      const canvasStream = composer.getStream()
      const stream = combineStreams(canvasStream, micStream, screenStream)

      // Create and start screen recorder
      screenRecorder = await createScreenRecorder(stream)

      await screenRecorder.start()

      startElapsedTimer()
      updateStore({ state: 'recording' })
    },

    async stop (): Promise<void> {
      const { state } = get(store)

      if (state !== 'recording' && state !== 'paused') {
        throw new Error(`Cannot stop from state: ${state}`)
      }

      if (screenRecorder === null) {
        throw new Error('No active recording')
      }

      updateStore({ state: 'stopping' })
      stopElapsedTimer()

      const result = await screenRecorder.stop()
      updateStore({ state: 'stopped', result })

      manager.cleanup()
      composer.cleanup()

      screenRecorder = null
    },

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Guard the call: only invoke `stop()` when the store's `state` is 'recording' or 'paused'.
  2. Keep a reference to the in-flight stop promise and reuse/await it instead of calling `stop()` again on double clicks.
  3. Use `recorder.cancel()` or `cleanup()` when you want to discard/reset from a non-active state instead of `stop()`.
  4. Disable the stop control unless `state === 'recording' || state === 'paused'`.

Example fix

// before
async function stopRecording () {
  await recorder.stop()
}

// after
let stopping: Promise<void> | null = null
async function stopRecording () {
  if (stopping !== null) return stopping
  const { state } = getStore(recorder)
  if (state !== 'recording' && state !== 'paused') return
  stopping = recorder.stop().finally(() => { stopping = null })
  return stopping
}
Defensive patterns

Strategy: validation

Validate before calling

const { state } = get(recorder)
if (state !== 'recording' && state !== 'paused') return // nothing to stop
await recorder.stop()

Type guard

function canStop(state: RecorderState): state is 'recording' | 'paused' {
  return state === 'recording' || state === 'paused'
}

Try / catch

try {
  await recorder.stop()
} catch (err) {
  if (err instanceof Error && err.message.startsWith('Cannot stop from state:')) {
    // already stopped or never started — safe to ignore and reset UI
  } else throw err
}

Prevention

When it happens

Trigger: Calling `recorder.stop()` twice (the second call sees 'stopped'); calling `stop()` before any recording started ('idle'/'ready'); calling `stop()` while a previous `stop()` is still awaiting `screenRecorder.stop()` ('stopping'); the stream-ended callback firing after `stop()` already transitioned the state.

Common situations: Double-click on a stop button; a UI calling stop both from a stream-ended event handler (`onStreamEnded`) and from a manual stop action; unmount cleanup code calling `stop()` after the user already stopped; tests tearing down while state is 'ready'.

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 hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/439c5827e916162d. Report an issue: GitHub.