actualbudget/actual · error · APIError

Cannot end a batch process: no batch started

Error message

Cannot end a batch process: no batch started

What it means

api.batchBudgetEnd() resolves the promise created by batchBudgetStart() and clears the module-level batchPromise. If no batch was started, there is nothing to resolve, so the handler throws this APIError.

Source

Thrown at packages/loot-core/src/server/api.ts:156

  // syncing layer in that case.
  if (IMPORT_MODE) {
    void db.asyncTransaction(() => {
      return new Promise((resolve, reject) => {
        batchPromise = { resolve, reject };
      });
    });
  } else {
    void batchMessages(() => {
      return new Promise((resolve, reject) => {
        batchPromise = { resolve, reject };
      });
    });
  }
};

handlers['api/batch-budget-end'] = async function () {
  if (!batchPromise) {
    throw APIError('Cannot end a batch process: no batch started');
  }

  batchPromise.resolve();
  batchPromise = null;
};

handlers['api/load-budget'] = async function ({ id }) {
  const { id: currentId } = prefs.getPrefs() || {};

  if (currentId !== id) {
    connection.send('start-load');
    const { error } = await handlers['load-budget']({ id });

    if (!error) {
      connection.send('finish-load');
    } else {
      connection.send('show-budgets');

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Only call batchBudgetEnd() after batchBudgetStart() succeeded — move the end call inside try/finally that starts after the try begins
  2. Guard with a flag: only end when a start was recorded
  3. Check for double-invocation (e.g. both a success path and a finally path calling end)

Example fix

// before
try {
  await api.batchBudgetStart();
  applyUpdates(items);
} finally {
  await api.batchBudgetEnd(); // throws if start threw
}
// after
await api.batchBudgetStart();
try {
  applyUpdates(items);
} finally {
  await api.batchBudgetEnd();
}
Defensive patterns

Strategy: validation

Validate before calling

if (!batchPromise) return;

Try / catch

if (!batchPromise) {
  logger.warn('batch end with no batch; ignoring');
  return;
}
batchPromise.resolve();
batchPromise = null;

Prevention

When it happens

Trigger: Calling api.batchBudgetEnd() without a preceding api.batchBudgetStart(), calling it twice, or calling it after a failed start (which never set batchPromise).

Common situations: Cleanup code in a finally block that runs even when batchBudgetStart() threw; duplicated end calls across retries; copy-pasted code paths sharing one end call.

Related errors


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