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 &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;
    }

View on GitHub (pinned to 3b9a59c288)

Solutions

  1. Wrap the label value in a template literal to force a string: `{ label: `${segment.label} ${segment.index + 1}` }`
  2. Or coerce explicitly with String(): `{ label: String(segment.index) }`
  3. If you do not intend to rename the segment, omit `label` from the returned object entirely
  4. 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

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


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