mifi/lossless-cut · warning · UserFacingError
"{{property}}" must be a number
Error message
"{{property}}" must be a number What it means
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.
Source
Thrown at src/renderer/src/hooks/useSegments.tsx:823
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;
}
const mutateSegments = async (expr: string) => (await pMap(cutSegments, async (seg, index) => ({
...seg,
...(seg.selected && await mutateSegment(seg, index, expr)),
}), { concurrency: 5 })).flat();View on GitHub (pinned to 3b9a59c288)
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
Example fix
// before (string concatenation -> '5' makes the sum a string)
{ start: segment.start + '5' }
// after
{ start: segment.start + 5 } Defensive patterns
Strategy: validation
Validate before calling
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 ('start' in r && typeof r.start !== 'number')
return '"start" must be a number';
// optional: reject NaN to avoid silent corruption downstream
if ('start' in r && Number.isNaN(r.start as number))
return '"start" must not be NaN';
return null;
} Type guard
function hasNumberStart(r: object): r is { start: number } & object {
return 'start' in r && typeof (r as { start?: unknown }).start === 'number';
} Try / catch
// Reuse the onSubmit catch at useSegments.tsx:851; the thrown UserFacingError
// already carries the interpolated message. No special-case branch needed:
catch (err) {
if (err instanceof Error)
return { error: i18n.t('Expression failed: {{errorMessage}}', { errorMessage: err.message }) };
throw err;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- "{{property}}" must be a string
- "{{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/62e8589cff33b542.
Report an issue: GitHub.