moeru-ai/airi · error · Error

timeout must be a positive finite number

Error message

timeout must be a positive finite number

What it means

Thrown by the screenCapture.setSource eventa invoke handler in the Electron main process when the optional timeout field of the request is a number but fails validation: it must be > 0, finite, and not NaN. The timeout controls how long the main process holds setSourceMutex while waiting for the renderer's getUserMedia/getDisplayMedia call to hit session.setDisplayMediaRequestHandler (default 5000 ms when omitted). The check runs before the mutex is acquired, so no state leaks when it throws. Note that a non-number value (e.g. undefined) is allowed and falls back to the 5s default.

Solutions

  1. Pass a positive finite number of milliseconds, e.g. timeout: 10_000, or omit the field entirely to use the built-in 5000 ms default.
  2. If the value comes from user config, clamp it before sending: Math.min(Math.max(Number(raw), 100), 60_000).
  3. Search the renderer code for the setSource call site and log/inspect the exact timeout value being serialized over IPC.
  4. If you intended 'wait forever', do not use Infinity; pick a large finite bound and release via screenCapture.resetSource(handle) when getDisplayMedia completes.

Example fix

// before
await invoke(screenCapture.setSource, {
  sourceId,
  options,
  timeout: remainingMs, // remainingMs can be 0 or negative after a countdown
})

// after
const timeout = Number.isFinite(remainingMs) && remainingMs > 0
  ? Math.min(remainingMs, 60_000)
  : undefined // falls back to the 5000 ms default
await invoke(screenCapture.setSource, { sourceId, options, timeout })
Defensive patterns

Strategy: validation

Validate before calling

function normalizeSetSourceTimeout(raw: unknown): number | undefined {
  if (raw == null) return undefined // host default: 5000 ms
  const n = Number(raw)
  if (!Number.isFinite(n) || n <= 0) return undefined // drop invalid, use default
  return Math.min(n, 60_000)
}

const timeout = normalizeSetSourceTimeout(settings.captureTimeout)

Type guard

function isPositiveFiniteTimeout(v: unknown): v is number {
  return typeof v === 'number' && Number.isFinite(v) && v > 0
}

Try / catch

try {
  await invoke(screenCapture.setSource, { sourceId, options, timeout })
} catch (error) {
  if (errorMessageFrom(error).includes('timeout must be a positive finite number')) {
    // config bug: fix the stored timeout and retry with the default
    await invoke(screenCapture.setSource, { sourceId, options })
  } else throw error
}

Prevention

When it happens

Trigger: Renderer calls screenCapture.setSource({ sourceId, options, timeout }) with timeout = 0, a negative number, Infinity, -Infinity, or NaN. Typical causes: computing timeout from a subtraction that yields 0 or a negative drift, parsing it from user input or settings JSON without validation, or passing Number.POSITIVE_INFINITY for 'no timeout'.

Common situations: A settings UI exposes a capture timeout slider that can reach 0; a config file written by an older app version contains 0; timeout is derived from a countdown that expired; JSON.parse turns 'Infinity' string handling into NaN via Number(...) on bad input.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/b055fa357d02be75. Report an issue: GitHub.

Appendix: source

Thrown at packages/electron-screen-capture/src/main/index.ts:226

  defineInvokeHandler(context, screenCapture.getSources, async (sourcesOptions) => {
    // NOTICE(@nekomeowww): In probability of 9/10, the window thumbnail is purely empty or black, sources printed and
    // nothing is returned from the desktopCapturer API.
    // NOTICE(@sumimakito): Not only thumbnail is empty, the appIcon could be empty as well with nothing returned.
    // REVIEW(@sumimakito): This has nothing to do with out side, probably related to Electron Bug, you can
    // read more here https://github.com/electron/electron/issues/44504
    const sources = await desktopCapturer.getSources(sourcesOptions)
    return sources.map(source => toSerializableDesktopCapturerSource(source))
  })

  defineInvokeHandler(context, screenCapture.setSource, async (request, eventaOptions) => {
    // FIXME: Would be better if `onlySameWindow` in `createContext` also filters out invocations here.
    if (window.webContents.id !== eventaOptions?.raw.ipcMainEvent.sender.id)
      return

    const { timeout } = request
    if (typeof timeout === 'number' && (timeout <= 0 || !Number.isFinite(timeout) || Number.isNaN(timeout))) {
      throw new Error('timeout must be a positive finite number')
    }

    await setSourceMutex.acquire()
    log.withFields({ windowId, windowTitle: tryWindowTitle(window, windowTitle) }).debug('setSourceMutex acquired')

    clearTimeout(setSourceMutexTimeoutHandle)
    const handle = nanoid()
    setSourceMutexTimeoutHandle = undefined
    screenCaptureSourceMutexHandle = handle

    try {
      session.setDisplayMediaRequestHandler(async (_request, callback) => {
        const sources = await desktopCapturer.getSources(request.options)
        const source = sources.find(source => source.id === request.sourceId)
        if (!source) {
          throw new Error(`Source with id ${request.sourceId} not found.`)
        }

View on GitHub (pinned to 677329427f)