actualbudget/actual · error
${validForMergeError} (one of: 'One of the provided transact
Error message
${validForMergeError} (one of: 'One of the provided transactions does not exist', 'Cannot merge transactions from different accounts', 'Cannot merge transactions with different amounts', 'Cannot merge transfers to different accounts') What it means
mapAndValidateTransactions fetches both transactions and runs validForMergeExplanation to check merge preconditions. If any precondition fails (nonexistent transaction, different accounts, different amounts, or transfers to different accounts), it throws with that explanation directly as the message — the template here just documents the four possible texts.
Source
Thrown at packages/loot-core/src/server/transactions/merge.ts:105
const [aTransfer, bTransfer] = await mapAndValidateTransactions(
aTransferId,
bTransferId,
);
await setTransfers([aTransfer.id, bTransfer.id], null);
return mergeTransactionsNoTransfer(aTransfer, bTransfer);
}
async function mapAndValidateTransactions(
aId: TransactionEntity['id'],
bId: TransactionEntity['id'],
): Promise<TransactionEntity[]> {
// get most recent transactions
const a: TransactionEntity = await db.getTransaction(aId);
const b: TransactionEntity = await db.getTransaction(bId);
const validForMergeError = validForMergeExplanation(a, b);
if (validForMergeError) {
throw new Error(validForMergeError);
}
return [a, b];
}
export async function mergeTransactionsNoTransfer(
a: TransactionEntity,
b: TransactionEntity,
): Promise<TransactionEntity['id']> {
const { keep, drop } = determineKeepDrop(a, b);
// Load subtransactions with a single query, then split by parent_id in memory
const keepSubtransactions: TransactionEntity[] = [];
const dropSubtransactions: TransactionEntity[] = [];
const parents: string[] = [];
if (keep.is_parent) parents.push(keep.id);
if (drop.is_parent) parents.push(drop.id);
let rows: TransactionEntity[] = [];View on GitHub (pinned to d4334cb6e6)
Solutions
- Read the thrown message — it states exactly which precondition failed — and surface it to the user.
- Before merging, verify both transactions exist, share the same account, and have identical amounts.
- Refresh the transaction list before merging to avoid merging stale/deleted rows.
- Exclude transfer transactions with mismatched counterpart accounts from duplicate candidates, or merge them via mergeTransactionsNoTransfer when appropriate.
Example fix
// before
await mergeTransactions([{ id: aId }, { id: bId }]);
// after
const [a, b] = await Promise.all([getTransaction(aId), getTransaction(bId)]);
if (!a || !b) throw new Error('One of the transactions no longer exists');
if (a.account !== b.account || a.amount !== b.amount) {
throw new Error('Transactions are not mergeable (account/amount differ)');
}
await mergeTransactions([{ id: aId }, { id: bId }]); Defensive patterns
Strategy: validation
Validate before calling
const [a, b] = await Promise.all([getTransaction(aId), getTransaction(bId)]);
const mergeable = a && b && a.account === b.account && a.amount === b.amount;
if (!mergeable) throw new Error('Selected transactions are not valid merge candidates'); Type guard
function isMergeable(a: TransactionEntity | null, b: TransactionEntity | null): boolean {
return !!a && !!b && a.account === b.account && a.amount === b.amount;
} Try / catch
try {
await mergeTransactions([{ id: aId }, { id: bId }]);
} catch (e) {
if (/does not exist|different accounts|different amounts|different transfers/.test(e.message)) {
showError(`Cannot merge: ${e.message}`);
} else { throw e; }
} Prevention
- Refresh transaction data immediately before merging to avoid deleted-row races
- Check account and amount equality in the duplicate-detection UI before offering merge
- Exclude transfers with mismatched counterpart accounts from merge candidates
When it happens
Trigger: Merging two transactions that (a) reference deleted/nonexistent ids, (b) live in different accounts, (c) have different amounts, or (d) are transfers whose counterparties point at different accounts. Called via mergeTransactions on user-selected 'duplicate' pairs that are actually not mergeable.
Common situations: Duplicate-detection heuristics matching on payee/date but not amount or account; one side of the pair deleted between selection and merge (race); a transaction edited by another client after selection changing its amount.
Related errors
- Merging is only possible with 2 transactions, but found ${JS
- `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/f64c9389470457c4.
Report an issue: GitHub.