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
- 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.
- If the value comes from user config, clamp it before sending: Math.min(Math.max(Number(raw), 100), 60_000).
- Search the renderer code for the setSource call site and log/inspect the exact timeout value being serialized over IPC.
- 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
- Never compute timeout from a subtraction without clamping; expired countdowns yield 0 or negatives.
- Validate user-configurable timeouts at the settings boundary (min 100 ms, max 60 s), not at the IPC boundary.
- Remember undefined is valid and selects the 5000 ms default — omit rather than pass Infinity.
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.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- initScreenCaptureForWindow should only be called once per…
- checkMacOSScreenCapturePermission is only available on…
- Electron IPC is not available in this renderer context
- Electron ipcRenderer is not available. Pass it explicitly…
- Extension folder import is available only from the…
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)