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
- Convert legacy string amounts to IntegerAmount numbers at the data boundary instead of relying on the string path
- Pre-parse with a dedicated parser (currencyToAmount / amountToInteger from shared/util) and handle failure before formatting
- Sanitize the string: strip non-numeric chars yourself and check the result is non-empty before calling
- 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
- Stop sending string amounts; always use IntegerAmount numbers (amountToInteger)
- Parse user input with currencyToAmount/fromEdit at the edit boundary, not at format time
- Treat 'N/A'/'—' placeholders as null/0 before they reach formatters
- Cover the string-coercion path with tests for empty and non-numeric strings
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
- An error occurred while parsing the template
- Unsupported template type reached migration
- Value is not a number (${typeof value}): ${value}
- Value is not a number (${typeof localValue}): ${localValue}
- Unknown format type: ${type}
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/5cebb53d0a53be42.
Report an issue: GitHub.