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
- Ensure exactly two transaction objects with populated 'id' fields are passed.
- Log the input array — the error message includes it — and fix the caller that built it.
- If the UI allows multi-select, disable merge or enforce a 2-row selection limit before calling.
- 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
- Enforce a 2-row selection limit in the UI before invoking merge
- Verify each selected row actually has an 'id' property (not transactionId/_id)
- Filter null/undefined entries out of the selection before calling
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
- ${validForMergeError} (one of: 'One of the provided transact
- `date` is required when adding a transaction
- Amount is invalid, must be an integer: ${trans.amount}
- Subtransaction amount is invalid, must be an integer: ${sub.
- Transaction ${id} does not belong to account ${accountId}
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/5f767d60ac91ff61.
Report an issue: GitHub.