actualbudget/actual · error

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

Error message

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

What it means

After the string-coercion branch, format re-checks that the value is a number before calling integerToCurrency. Values that are neither null/empty-string nor string nor number (e.g. booleans, objects, arrays, undefined after non-empty check) fall through to this check and throw 'Value is not a number (<typeof>): <value>'.

Source

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

        // 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,
        ),
      };
    }
    default:
      throw new Error('Unknown format type: ' + type);
  }
}

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Extract the numeric field from the object before formatting (format(obj.amount, 'financial'))
  2. Coerce explicitly with Number(value) and validate isFinite before the call
  3. Fix the type at the source so the value is an IntegerAmount (number)
  4. Add a runtime guard at the call site that falls back to 0 for non-numeric values

Example fix

// before
format({ amount: 12300 }, 'financial'); // object -> throws
// after
format(12300, 'financial'); // pass IntegerAmount number
Defensive patterns

Strategy: type-guard

Validate before calling

if (value != null && typeof value !== 'string' && typeof value !== 'number') {
  throw new TypeError(`financial format needs number|string|null, got ${typeof value}`);
}

Type guard

function isFormatableFinancialValue(v: unknown): v is number | string | null | undefined | '' {
  return v == null || v === '' || typeof v === 'string' || typeof v === 'number';
}

Try / catch

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

Prevention

When it happens

Trigger: Passing true/false, an object like {amount: 123}, an array, NaN is a number so it passes this check but not undefined or Symbol to a 'financial' format type; also values where null/'' normalization was skipped because the value was undefined (undefined !== null? actually null==null caught, but undefined === '' false, undefined == null true — so mainly objects/booleans).

Common situations: A query aggregation returning {value: n} objects instead of raw numbers; boolean flags accidentally bound into amount fields; refactors changing IntegerAmount to a wrapped object.

Related errors


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