actualbudget/actual · error

Unknown payee name normalization: ${String(normalization)}

Error message

Unknown payee name normalization: ${String(normalization)}

What it means

normalizePayeeName applies one of a fixed set of normalizations ('original' or 'title-case') to payee names during transaction sync. A `satisfies never` guard makes the switch exhaustive at compile time; at runtime an unrecognized value falls through to this throw. It protects the sync pipeline from invalid normalization options coming from callers.

Source

Thrown at packages/loot-core/src/server/accounts/sync.ts:430

  return trans.payee;
}

export const PAYEE_NAME_NORMALIZATIONS = ['original', 'title-case'] as const;
export type PayeeNameNormalization = (typeof PAYEE_NAME_NORMALIZATIONS)[number];

function normalizePayeeName(
  payeeName: string,
  normalization: PayeeNameNormalization,
): string {
  switch (normalization) {
    case 'original':
      return payeeName;
    case 'title-case':
      return title(payeeName);
    default:
      normalization satisfies never;
      throw new Error(
        `Unknown payee name normalization: ${String(normalization)}`,
      );
  }
}

async function normalizeTransactions(
  transactions,
  acctId,
  {
    payeeNameNormalization = 'title-case',
  }: {
    payeeNameNormalization?: PayeeNameNormalization;
  } = {},
) {
  const payeesToCreate = new Map();

  const normalized = [];
  for (let trans of transactions) {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Change the normalization value to exactly 'original' or 'title-case'.
  2. Check the option's source (API payload, config) for typos and casing ('title-case', not 'titlecase').
  3. Pin your API client version to match the running Actual version.
  4. Log the incoming normalization value at the boundary to catch bad payloads early.

Example fix

// before
normalizeTransactions({ normalization: 'titlecase' });
// after
normalizeTransactions({ normalization: 'title-case' }); // 'original' | 'title-case'
Defensive patterns

Strategy: type-guard

Validate before calling

const NORMALIZATIONS = ['original', 'title-case'] as const;
type Normalization = (typeof NORMALIZATIONS)[number];
if (!NORMALIZATIONS.includes(opts.normalization as Normalization)) {
  throw new Error(`normalization must be one of ${NORMALIZATIONS.join(', ')}`);
}

Type guard

function isNormalization(v: unknown): v is 'original' | 'title-case' {
  return v === 'original' || v === 'title-case';
}

Try / catch

try {
  await importTransactions(accountId, txs, opts);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Unknown payee name normalization')) {
    console.error('Fix the normalization option; use "original" or "title-case"');
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling the transactions-sync path (normalizeTransactions, e.g. via importTransactions) with a payee-name normalization value other than 'original' or 'title-case' — e.g. 'titlecase', 'capitalize', or an option from a mismatched client version.

Common situations: API integrations passing free-form strings for the option; version drift where a caller uses an option added in a newer/older version than the running core; scripts copying option names with wrong spelling/casing.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


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