{"record":{"id":"86bdd2dab52c1694","repo":"mifi/lossless-cut","slug":"property-must-be-a-string","errorCode":null,"errorMessage":"\"{{property}}\" must be a string","messagePattern":"\"(.+?)\\}\" must be a string","errorType":"exception","errorClass":"UserFacingError","httpStatus":null,"severity":"warning","filePath":"src/renderer/src/hooks/useSegments.tsx","lineNumber":819,"sourceCode":"            { name: i18n.t('Segment label (regexp)'), code: '/^My label/.test(segment.label)' },\n            { name: i18n.t('Segment tag value'), code: \"segment.tags.myTag === 'tag value'\" },\n            { name: i18n.t('Markers'), code: 'segment.end == null' },\n          ]}\n          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","sourceCodeStart":801,"sourceCodeEnd":837,"githubUrl":"https://github.com/mifi/lossless-cut/blob/3b9a59c288bf6e11076b583c932cfa48ddab3b02/src/renderer/src/hooks/useSegments.tsx#L801-L837","documentation":"Thrown by mutateSegment (inside mutateSegmentsByExpr, useSegments.tsx:819) when the object returned by a user's 'Edit segments by expression' JS expression contains a `label` property whose value is not a JavaScript string. `label` maps to the segment's display name (StateSegment.name), so any non-string is rejected before mutation. The error is a UserFacingError, caught at useSegments.tsx:851 and shown to the user as 'Expression failed: \"label\" must be a string'.","triggerScenarios":"Entering an expression whose returned object has a non-string label, e.g. `{ label: segment.index }` (number), `{ label: true }` (boolean), `{ label: [segment.label] }` (array), or `{ label: segment.tags }` (object). Numeric arithmetic on a label also yields a number: `{ label: segment.label + 1 }` when segment.label is numeric.","commonSituations":"Users forget that label must stay a string after transformation (e.g. appending an index without a template literal). Copy-pasting the 'Add number suffix to label' example but replacing the template literal with raw arithmetic. Returning a tag value or index directly as the label.","solutions":["Wrap the label value in a template literal to force a string: `{ label: `${segment.label} ${segment.index + 1}` }`","Or coerce explicitly with String(): `{ label: String(segment.index) }`","If you do not intend to rename the segment, omit `label` from the returned object entirely","Verify the type at runtime in the expression: `{ ...(typeof myLabel === 'string' && { label: myLabel }) }`"],"exampleFix":"// before\n{ label: segment.index }\n\n// after\n{ label: `${segment.label} ${segment.index + 1}` }","handlingStrategy":"validation","validationCode":"// Before mutating, check the expression's returned object shape.\n// Run this against the response from safeishEval in mutateSegment.\nfunction 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 ('label' in r && typeof r.label !== 'string')\n    return '\"label\" must be a string';\n  return null;\n}","typeGuard":"// Type guard narrowing the label field.\nfunction hasStringLabel(r: object): r is { label: string } & object {\n  return 'label' in r && typeof (r as { label?: unknown }).label === 'string';\n}","tryCatchPattern":"// onSubmit already wraps mutateSegments in try/catch (useSegments.tsx:844-856).\n// Surface UserFacingError.message verbatim to the dialog:\ntry {\n  const mutated = await mutateSegments(value);\n  safeSetCutSegments(mutated, fileDuration);\n  return undefined;\n} catch (err) {\n  if (err instanceof Error)\n    return { error: i18n.t('Expression failed: {{errorMessage}}', { errorMessage: err.message }) };\n  throw err;\n}","preventionTips":["Always build label values with template literals (backticks) so the result is provably a string","When reusing examples from the dialog, keep the template literal — do not replace it with raw arithmetic","If label is optional, omit the key rather than passing undefined"],"tags":["validation","user-input","expression-eval","typescript","i18n"],"backgroundTag":null,"analyzedSha":"3b9a59c288bf6e11076b583c932cfa48ddab3b02","analyzedAt":"2026-08-12T20:54:25.651Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}