actualbudget/actual · error

Value is not a number (${typeof value}): ${value}

Error message

Value is not a number (${typeof value}): ${value}

What it means

The format function's 'number' case requires the raw value to be a JS number so Intl-based formatter.format(value) can run. If value is a string, null, undefined, object, etc., it throws 'Value is not a number (<typeof>): <value>'. Note 'financial' types coerce strings, but 'number' deliberately does not.

Source

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

function format(
  value: unknown,
  type: FormatType,
  formatter: { format: (value: number) => string },
  decimalPlaces: number,
): FormatResult {
  switch (type) {
    case 'string': {
      const val = JSON.stringify(value);

      if (val.charAt(0) === '"' && val.charAt(val.length - 1) === '"') {
        return { formattedString: val.slice(1, -1) };
      }
      return { formattedString: val };
    }
    case 'number':
      if (typeof value !== 'number') {
        throw new Error(
          'Value is not a number (' + typeof value + '): ' + value,
        );
      }
      return { numericValue: value, formattedString: formatter.format(value) };
    case 'percentage':
      return { formattedString: value + '%' };
    case 'financial-with-sign':
    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.

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Coerce the value before formatting: Number(value) after checking it's not NaN
  2. Pass the 'financial' type instead if the value may be an integer-amount string and currency formatting is acceptable
  3. Fix the data source (query select / API mapping) to return numbers, not strings
  4. Guard the call site: typeof v === 'number' ? format(v, 'number') : format(0, 'number')

Example fix

// before
format(row.total as unknown, 'number'); // row.total is '123' (string)
// after
const n = Number(row.total);
format(Number.isFinite(n) ? n : 0, 'number');
Defensive patterns

Strategy: type-guard

Validate before calling

if (typeof value !== 'number' || !Number.isFinite(value)) {
  throw new TypeError(`format('number') requires a finite number, got ${typeof value}`);
}

Type guard

function isNumeric(v: unknown): v is number {
  return typeof v === 'number' && Number.isFinite(v);
}

Try / catch

let out: string;
try {
  out = format(value, 'number');
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Value is not a number')) {
    out = format(0, 'number');
  } else throw e;
}

Prevention

When it happens

Trigger: Calling format(value, 'number') with a string like '123' (common after fetching data that wasn't coerced), null/undefined, NaN-adjacent values from a spreadsheet cell, or an object returned by a query aggregation.

Common situations: Binding a report/CSV column configured as 'number' to data that arrives as strings from an API or AQL query; passing state from an uncontrolled input (always a string) directly to the formatter; a refactor changing the data type upstream.

Related errors


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