mifi/lossless-cut · warning · UserFacingError
"{{property}}" must be a string
Error message
"{{property}}" must be a string What it means
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'.
Source
Thrown at src/renderer/src/hooks/useSegments.tsx:819
{ name: i18n.t('Segment label (regexp)'), code: '/^My label/.test(segment.label)' },
{ name: i18n.t('Segment tag value'), code: "segment.tags.myTag === 'tag value'" },
{ name: i18n.t('Markers'), code: 'segment.end == null' },
]}
title={i18n.t('Select segments by expression')}
description={<Trans>Enter a JavaScript expression which will be evaluated for each segment. Segments for which the expression evaluates to "true" will be selected. <button type="button" className="link-button" onClick={() => mainApi.openExternal(selectSegmentByExpressionHelpUrl)}>View available syntax.</button></Trans>}
variables={['segment.index', 'segment.label', 'segment.start', 'segment.end', 'segment.duration', 'segment.tags.*']}
/>
),
});
}, [showGenericDialog, t, getScopeSegment, cutSegments, selectSegments]);
const mutateSegmentsByExpr = useCallback(async () => {
async function mutateSegment(seg: StateSegment, index: number, expr: string) {
const response = (await safeishEval(expr, { segment: getScopeSegment(seg, index) }));
invariant(typeof response === 'object' && response != null, i18n.t('The expression must return an object'));
const ret: Partial<Pick<StateSegment, 'name' | 'start' | 'end' | 'tags'>> = {};
if ('label' in response) {
if (typeof response.label !== 'string') throw new UserFacingError(i18n.t('"{{property}}" must be a string', { property: 'label' }));
ret.name = response.label;
}
if ('start' in response) {
if (typeof response.start !== 'number') throw new UserFacingError(i18n.t('"{{property}}" must be a number', { property: 'start' }));
ret.start = response.start;
}
if ('end' in response) {
if (!(typeof response.end === 'number' || response.end === undefined)) throw new UserFacingError(i18n.t('"{{property}}" must be a number', { property: 'end' }));
ret.end = response.end;
}
if ('tags' in response) {
const tags = segmentTagsSchema.safeParse(response.tags);
if (!tags.success) throw new UserFacingError(i18n.t('"{{property}}" must be an object of strings', { property: 'tags' }));
ret.tags = tags.data;
}
return ret;
}
View on GitHub (pinned to 3b9a59c288)
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 }) }`
Example fix
// before
{ label: segment.index }
// after
{ label: `${segment.label} ${segment.index + 1}` } Defensive patterns
Strategy: validation
Validate before calling
// Before mutating, check the expression's returned object shape.
// Run this against the response from safeishEval in mutateSegment.
function validateMutateResponse(response: unknown): string | null {
if (typeof response !== 'object' || response === null)
return 'The expression must return an object';
const r = response as Record<string, unknown>;
if ('label' in r && typeof r.label !== 'string')
return '"label" must be a string';
return null;
} Type guard
// Type guard narrowing the label field.
function hasStringLabel(r: object): r is { label: string } & object {
return 'label' in r && typeof (r as { label?: unknown }).label === 'string';
} Try / catch
// onSubmit already wraps mutateSegments in try/catch (useSegments.tsx:844-856).
// Surface UserFacingError.message verbatim to the dialog:
try {
const mutated = await mutateSegments(value);
safeSetCutSegments(mutated, fileDuration);
return undefined;
} catch (err) {
if (err instanceof Error)
return { error: i18n.t('Expression failed: {{errorMessage}}', { errorMessage: err.message }) };
throw err;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- "{{property}}" must be a number
- "{{property}}" must be an object of strings
- No rows found
- Invalid start or end value. Must contain a number of seconds
- Segment start time must precede end time
AI-assisted analysis of mifi/lossless-cut@3b9a59c288 (2026-08-12).
Data as JSON: /api/errors/86bdd2dab52c1694.
Report an issue: GitHub.