actualbudget/actual · error · APIError

transfer account can not be the account being closed

Error message

transfer account can not be the account being closed

What it means

closeAccount rejects attempts to close an account into itself: if the supplied transferAccountId equals the id of the account being closed, the transfer would be a no-op while the account's balance would silently vanish, so the server throws 'transfer account can not be the account being closed'.

Source

Thrown at packages/loot-core/src/server/accounts/app.ts:671

              id: row.transfer_id,
              payee: null,
              transfer_id: null,
            });
          }

          void db.deleteTransaction({ id: row.id });
        });

        void db.deleteAccount({ id });
        void db.deleteTransferPayee({ id: transferPayee.id });
      });
    } else {
      if (balance !== 0 && transferAccountId == null) {
        throw APIError('balance is non-zero: transferAccountId is required');
      }

      if (id === transferAccountId) {
        throw APIError('transfer account can not be the account being closed');
      }

      await db.update('accounts', { id, closed: 1 });

      // If there is a balance we need to transfer it to the specified
      // account (and possibly categorize it)
      if (balance !== 0 && transferAccountId) {
        const transferPayee = await db.first<Pick<db.DbPayee, 'id'>>(
          'SELECT id FROM payees WHERE transfer_acct = ?',
          [transferAccountId],
        );

        if (!transferPayee) {
          throw new Error(
            `Transfer payee with account ID ${transferAccountId} not found.`,
          );
        }

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Choose a different transferAccountId than the account being closed.
  2. If no transfer is needed, bring the balance to zero first and close without transferAccountId.
  3. Add a client-side check id !== transferAccountId before invoking the API.

Example fix

// before
await api.closeAccount({ id: accId, transferAccountId: accId });
// after
const target = accId === savingsId ? checkingId : savingsId;
await api.closeAccount({ id: accId, transferAccountId: target });
Defensive patterns

Strategy: validation

Validate before calling

if (transferAccountId === accountId) {
  throw new Error('transferAccountId must differ from the account being closed');
}

Type guard

function hasValidTransferTarget(a: { id: string; transferAccountId?: string }): boolean {
  return a.transferAccountId == null || a.transferAccountId !== a.id;
}

Try / catch

try {
  await api.closeAccount({ id: accountId, transferAccountId });
} catch (e) {
  if (e.message.includes('can not be the account being closed')) {
    transferAccountId = await promptForDifferentTransferAccount();
    return closeAccountSafely(accountId, transferAccountId);
  }
}

Prevention

When it happens

Trigger: Calling closeAccount with { id: X, transferAccountId: X } for a non-zero-balance account — the destination account for the balance transfer is the same account being closed.

Common situations: Scripts that set transferAccountId from the same variable as the account id (copy-paste or templating bug); UI integrations that default the transfer dropdown to the closing account; batch-closing code that assigns a fallback transfer account without checking identity.

Related errors


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