{"record":{"id":"62e8589cff33b542","repo":"mifi/lossless-cut","slug":"property-must-be-a-number","errorCode":null,"errorMessage":"\"{{property}}\" must be a number","messagePattern":"\"(.+?)\\}\" must be a number","errorType":"exception","errorClass":"UserFacingError","httpStatus":null,"severity":"warning","filePath":"src/renderer/src/hooks/useSegments.tsx","lineNumber":823,"sourceCode":"          title={i18n.t('Select segments by expression')}\n          description={<Trans>Enter a JavaScript expression which will be evaluated for each segment. Segments for which the expression evaluates to &quot;true&quot; will be selected. <button type=\"button\" className=\"link-button\" onClick={() => mainApi.openExternal(selectSegmentByExpressionHelpUrl)}>View available syntax.</button></Trans>}\n          variables={['segment.index', 'segment.label', 'segment.start', 'segment.end', 'segment.duration', 'segment.tags.*']}\n        />\n      ),\n    });\n  }, [showGenericDialog, t, getScopeSegment, cutSegments, selectSegments]);\n\n  const mutateSegmentsByExpr = useCallback(async () => {\n    async function mutateSegment(seg: StateSegment, index: number, expr: string) {\n      const response = (await safeishEval(expr, { segment: getScopeSegment(seg, index) }));\n      invariant(typeof response === 'object' && response != null, i18n.t('The expression must return an object'));\n      const ret: Partial<Pick<StateSegment, 'name' | 'start' | 'end' | 'tags'>> = {};\n      if ('label' in response) {\n        if (typeof response.label !== 'string') throw new UserFacingError(i18n.t('\"{{property}}\" must be a string', { property: 'label' }));\n        ret.name = response.label;\n      }\n      if ('start' in response) {\n        if (typeof response.start !== 'number') throw new UserFacingError(i18n.t('\"{{property}}\" must be a number', { property: 'start' }));\n        ret.start = response.start;\n      }\n      if ('end' in response) {\n        if (!(typeof response.end === 'number' || response.end === undefined)) throw new UserFacingError(i18n.t('\"{{property}}\" must be a number', { property: 'end' }));\n        ret.end = response.end;\n      }\n      if ('tags' in response) {\n        const tags = segmentTagsSchema.safeParse(response.tags);\n        if (!tags.success) throw new UserFacingError(i18n.t('\"{{property}}\" must be an object of strings', { property: 'tags' }));\n        ret.tags = tags.data;\n      }\n      return ret;\n    }\n\n    const mutateSegments = async (expr: string) => (await pMap(cutSegments, async (seg, index) => ({\n      ...seg,\n      ...(seg.selected && await mutateSegment(seg, index, expr)),\n    }), { concurrency: 5 })).flat();","sourceCodeStart":805,"sourceCodeEnd":841,"githubUrl":"https://github.com/mifi/lossless-cut/blob/3b9a59c288bf6e11076b583c932cfa48ddab3b02/src/renderer/src/hooks/useSegments.tsx#L805-L841","documentation":"Thrown by mutateSegment (useSegments.tsx:823) when the object returned by the user's expression contains a `start` property that is not of type 'number'. `start` is the segment's start time in seconds and is stored numerically; unlike `end`, even `undefined`/`null` are rejected here (only omission is tolerated). Surfaced as 'Expression failed: \"start\" must be a number' via the onSubmit catch block at useSegments.tsx:851.","triggerScenarios":"Returning `{ start: '5' }` (string), `{ start: segment.start + '5' }` (string concatenation because one operand is a string), `{ start: null }`, `{ start: undefined }`, or `{ start: Number(segment.start) === NaN }` (NaN is technically typeof number but will corrupt timing downstream — though it passes this guard). Any arithmetic mixing a string operand yields a string and trips the guard.","commonSituations":"Using `+` where one side is a string (e.g. reading a tag value like `segment.tags.offset` and adding it to segment.start). Forgetting that marker segments still have a numeric `start` but a null `end`, then writing `{ start: segment.end }` which copies null. Passing user-pasted input without Number() coercion.","solutions":["Ensure both operands are numeric so `+` adds instead of concatenates: `{ start: segment.start + 5 }` (no quotes around 5)","Coerce explicitly when a value may be stringly: `{ start: segment.start + Number(segment.tags.offset) }`","Use unary `+` to force numeric: `{ start: +segment.tags.offset }`","Omit `start` from the returned object if you are not changing it"],"exampleFix":"// before (string concatenation -> '5' makes the sum a string)\n{ start: segment.start + '5' }\n\n// after\n{ start: segment.start + 5 }","handlingStrategy":"validation","validationCode":"function validateMutateResponse(response: unknown): string | null {\n  if (typeof response !== 'object' || response === null)\n    return 'The expression must return an object';\n  const r = response as Record<string, unknown>;\n  if ('start' in r && typeof r.start !== 'number')\n    return '\"start\" must be a number';\n  // optional: reject NaN to avoid silent corruption downstream\n  if ('start' in r && Number.isNaN(r.start as number))\n    return '\"start\" must not be NaN';\n  return null;\n}","typeGuard":"function hasNumberStart(r: object): r is { start: number } & object {\n  return 'start' in r && typeof (r as { start?: unknown }).start === 'number';\n}","tryCatchPattern":"// Reuse the onSubmit catch at useSegments.tsx:851; the thrown UserFacingError\n// already carries the interpolated message. No special-case branch needed:\ncatch (err) {\n  if (err instanceof Error)\n    return { error: i18n.t('Expression failed: {{errorMessage}}', { errorMessage: err.message }) };\n  throw err;\n}","preventionTips":["Never quote numeric literals in expressions (`5`, not `'5'`)","When mixing segment.start with tag-derived values, wrap them in Number() first","Remember marker segments have a numeric `start` but null `end` — do not copy end into start"],"tags":["validation","numeric","expression-eval","user-input","i18n"],"backgroundTag":null,"analyzedSha":"3b9a59c288bf6e11076b583c932cfa48ddab3b02","analyzedAt":"2026-08-12T20:54:25.651Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}