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 &quot;true&quot; 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

  1. Ensure both operands are numeric so `+` adds instead of concatenates: `{ start: segment.start + 5 }` (no quotes around 5)
  2. Coerce explicitly when a value may be stringly: `{ start: segment.start + Number(segment.tags.offset) }`
  3. Use unary `+` to force numeric: `{ start: +segment.tags.offset }`
  4. 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

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


AI-assisted analysis of mifi/lossless-cut@3b9a59c288 (2026-08-12). Data as JSON: /api/errors/62e8589cff33b542. Report an issue: GitHub.