actualbudget/actual · error

Invalid numeric value: ${localValue}

Error message

Invalid numeric value: ${localValue}

What it means

For 'financial' format types, string values are accepted for backwards compatibility but are strictly cleaned (strip everything except digits and minus) and parsed with parseInt. If nothing numeric remains (isNaN), format throws `Invalid numeric value: ${localValue}`. This rejects strings that contain no recoverable integer amount.

Source

Thrown at packages/desktop-client/src/hooks/useFormat.ts:85

    case 'financial-no-decimals':
    case 'financial': {
      let localValue = value;
      if (localValue == null || localValue === '') {
        localValue = 0;
      } else if (typeof localValue === 'string') {
        // This case is generally flawed, but we need to support it for
        // backwards compatibility for now.
        // For example, it is not clear how the string might look like
        // The Budget sends 12300, if the user inputs 123.00, but
        // there might be other components that send 123 with the same user input.
        // Ideally the string case will be removed in the future. We should always
        // use the IntegerAmount.
        // The parseInt with the replace is a workaround for the case and looks like
        // the "least wrong" solution.
        const integerString = localValue.replace(/[^\d-]/g, '');
        const parsed = parseInt(integerString, 10);
        if (isNaN(parsed)) {
          throw new Error(`Invalid numeric value: ${localValue}`);
        }
        localValue = parsed;
      }

      if (typeof localValue !== 'number') {
        throw new Error(
          'Value is not a number (' + typeof localValue + '): ' + localValue,
        );
      }

      return {
        numericValue: localValue,
        formattedString: integerToCurrency(
          localValue,
          formatter,
          decimalPlaces,
        ),
      };

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Convert legacy string amounts to IntegerAmount numbers at the data boundary instead of relying on the string path
  2. Pre-parse with a dedicated parser (currencyToAmount / amountToInteger from shared/util) and handle failure before formatting
  3. Sanitize the string: strip non-numeric chars yourself and check the result is non-empty before calling
  4. Show an edit state to the user instead of formatting when the value can't be parsed

Example fix

// before
format('N/A', 'financial'); // throws
// after
const parsed = currencyToAmount('N/A');
format(parsed == null || Number.isNaN(parsed) ? 0 : parsed, 'financial');
Defensive patterns

Strategy: validation

Validate before calling

const cleaned = typeof v === 'string' ? v.replace(/[^\d-]/g, '') : '';
if (typeof v === 'string' && (cleaned === '' || Number.isNaN(parseInt(cleaned, 10)))) {
  return 0; // or reject before formatting
}

Type guard

function isParsableAmountString(v: unknown): v is string {
  return typeof v === 'string' && v.replace(/[^\d-]/g, '') !== ''
    && !Number.isNaN(parseInt(v.replace(/[^\d-]/g, ''), 10));
}

Try / catch

let out: string;
try {
  out = format(value, 'financial');
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Invalid numeric value')) {
    out = format(0, 'financial');
  } else throw e;
}

Prevention

When it happens

Trigger: Passing a non-numeric string like 'abc', '12,3.45abc' that strips to '12345' is fine, but '', 'N/A', '-' alone, or '1.2.3' variants that strip to nothing hit this throw. Also currency symbols only, or boolean true (stringified path) with no digits.

Common situations: Legacy components sending raw user input (e.g. '12.00' vs '12300' integer amounts) where the input was cleared or contained letters; spreadsheet cells containing text; import/API data with placeholder strings like '—' or 'n/a'.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/5cebb53d0a53be42. Report an issue: GitHub.