actualbudget/actual · error · APIError

Cannot start a batch process: batch already started

Error message

Cannot start a batch process: batch already started

What it means

api.batchBudgetStart() opens a single mutation/transaction scope for bulk updates. The module keeps a module-level batchPromise; if a batch is already open, a second start would nest transactions, so the API throws instead.

Source

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

    throw APIError(`${debug}: category "${id}" does not exist`);
  }

  if (row.is_income !== 0) {
    throw APIError(`${debug}: category "${id}" is not an expense category`);
  }
}

function checkFileOpen() {
  if (!(prefs.getPrefs() || {}).id) {
    throw APIError('No budget file is open');
  }
}

let batchPromise = null;

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

  // If we are importing, all we need to do is start a raw database
  // transaction. Updating spreadsheet cells doesn't go through the
  // 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 };
      });
    });
  }

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Ensure every batchBudgetStart() is paired with batchBudgetEnd() in a try/finally
  2. Track a boolean in your code so only one batch runs at a time, or serialize callers
  3. If a stale batch is stuck (e.g. after a crash), restart the API process to reset the module state

Example fix

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

Strategy: validation

Validate before calling

if (batchPromise) throw APIError('Cannot start a batch process: batch already started');

Try / catch

if (batchPromise) {
  try {
    await batchPromise.resolve();
  } catch (e) {
    logger.error('stale batch', e);
  }
}

Prevention

When it happens

Trigger: Calling api.batchBudgetStart() twice without an intervening api.batchBudgetEnd() — e.g. concurrent code paths both starting a batch, or a previous batch never ended because of an exception between start and end.

Common situations: Import scripts where an earlier iteration threw inside the batch and the end call was skipped; parallel workers sharing one API instance; re-entrant calls from event handlers.

Related errors


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