actualbudget/actual · error

Merging is only possible with 2 transactions, but found ${JS

Error message

Merging is only possible with 2 transactions, but found ${JSON.stringify(transactions)}

What it means

mergeTransactions merges exactly two duplicate transactions into one. Before doing anything it maps the input to ids and filters out falsy values; if the resulting list is not exactly length 2 it throws, embedding the full JSON of the input array so you can see what was actually passed.

Source

Thrown at packages/loot-core/src/server/transactions/merge.ts:19

import { aqlQuery } from '#server/aql';
import * as db from '#server/db';
import { validForMergeExplanation } from '#shared/merge';
import { q } from '#shared/query';
import {
  deleteTransaction as sharedDeleteTransaction,
  ungroupTransactions,
} from '#shared/transactions';
import type { TransactionEntity } from '#types/models';

import { batchUpdateTransactions } from '.';

export async function mergeTransactions(
  transactions: Pick<TransactionEntity, 'id'>[],
): Promise<TransactionEntity['id']> {
  // make sure all values have ids
  const txIds = transactions?.map(x => x?.id).filter(Boolean) || [];
  if (txIds.length !== 2) {
    throw new Error(
      'Merging is only possible with 2 transactions, but found ' +
        JSON.stringify(transactions),
    );
  }
  const [a, b] = await mapAndValidateTransactions(txIds[0], txIds[1]);
  const aTransferId = a.transfer_id;
  const bTransferId = b.transfer_id;

  // we don't need all the transfer logic if there are no transfers.
  if (!aTransferId && !bTransferId) return mergeTransactionsNoTransfer(a, b);

  const transferAccount = aTransferId ? a.payee : b.payee;

  await setTransfers([a.id, b.id, aTransferId, bTransferId], null);
  const transferId = await mergeTransfers(aTransferId, bTransferId);
  const keptTxId = await mergeTransactionsNoTransfer(a, b);
  if (transferId) {
    await db.updateTransaction({

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Ensure exactly two transaction objects with populated 'id' fields are passed.
  2. Log the input array — the error message includes it — and fix the caller that built it.
  3. If the UI allows multi-select, disable merge or enforce a 2-row selection limit before calling.
  4. Verify the id field name matches TransactionEntity ('id'), not 'transactionId' or '_id'.

Example fix

// before
await mergeTransactions(selectedRows); // may be 1, 3, or ids missing
// after
const ids = selectedRows.map(r => r.id).filter(Boolean);
if (ids.length !== 2) {
  throw new Error(`Select exactly 2 duplicates to merge (got ${ids.length})`);
}
await mergeTransactions(selectedRows.slice(0, 2));
Defensive patterns

Strategy: validation

Validate before calling

function canMerge(transactions: { id?: string }[]): boolean {
  return (transactions?.map(t => t?.id).filter(Boolean) || []).length === 2;
}
if (!canMerge(selected)) throw new Error('Merge requires exactly 2 transactions with ids');

Type guard

function isMergeablePair(txns: unknown): txns is [{ id: string }, { id: string }] {
  return Array.isArray(txns) && txns.length === 2 &&
    txns.every(t => typeof t === 'object' && t !== null && typeof (t as any).id === 'string');
}

Try / catch

try {
  await mergeTransactions(pair);
} catch (e) {
  if (e.message.startsWith('Merging is only possible with 2 transactions')) {
    showError('Select exactly two duplicates to merge');
  } else { throw e; }
}

Prevention

When it happens

Trigger: Calling mergeTransactions with fewer or more than 2 transactions, passing transactions whose objects lack an 'id' property (filtered out by .filter(Boolean)), passing null/undefined in the array, or a UI selection bug allowing 3+ rows to be selected for merge.

Common situations: Bulk-selection UI allowing multi-row merge; table rows where the id field was named differently (transactionId) so id is undefined; passing the result of a multi-select instead of a pair.

Related errors


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