actualbudget/actual · error · BankSyncError

NO_DATA

NO_DATA

Error message

NO_DATA

What it means

After a successful SimpleFin response, the code checks Object.keys(res).length === 0 and throws BankSyncError('NO_DATA', 'NO_DATA') (sync.ts:219-221). This means SimpleFin returned an empty object — no account data at all — even though the request itself completed. It typically indicates the account ids or date range matched nothing on the SimpleFin side.

Source

Thrown at packages/loot-core/src/server/accounts/sync.ts:220

    res = await post(
      getServer().SIMPLEFIN_SERVER + '/transactions',
      {
        accountId: acctId,
        startDate: since,
      },
      {
        'X-ACTUAL-TOKEN': userToken,
      },
      // 5 minute timeout for batch sync, one minute for individual accounts
      Array.isArray(acctId) ? 300000 : 60000,
    );
  } catch (error) {
    logger.error('Suspected timeout during bank sync:', error);
    throw BankSyncError('TIMED_OUT', 'TIMED_OUT');
  }

  if (Object.keys(res).length === 0) {
    throw BankSyncError('NO_DATA', 'NO_DATA');
  }
  if (res.error_code) {
    throw BankSyncError(res.error_type, res.error_code);
  }

  let retVal = {};
  if (batchSync) {
    const batchErrors = res.errors;
    for (const accountId of Object.keys(res)) {
      if (accountId === 'errors') continue;

      const data = res[accountId];
      const error = batchErrors?.[accountId]?.[0];

      retVal[accountId] = {
        transactions: data?.transactions?.all,
        accountBalance: data?.balances,
        startingBalance: data?.startingBalance,

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Re-fetch the SimpleFin accounts list (GET on the SimpleFin access URL) and confirm the acctId still exists, then resync with the current id.
  2. Re-run the SimpleFin claims/setup flow to refresh the access URL if the account set changed.
  3. Remove any startDate restriction or widen the date range so data falls inside the window.
  4. Check the account on SimpleFin's dashboard for pending re-authentication of the underlying bank.
Defensive patterns

Strategy: fallback

Validate before calling

// Confirm the account exists in SimpleFin before syncing:
const accounts = await fetch(simplefinAccessUrl + '/accounts', {...}).then(r => r.json());
if (!accounts.accounts?.some(a => a.id === acctId)) {
  throw new Error(`Account ${acctId} not present in SimpleFin — refresh the access URL`);
}

Type guard

function isEmptyResponse(res: unknown): boolean {
  return typeof res === 'object' && res !== null && Object.keys(res).length === 0;
}

Try / catch

try {
  await syncAccount(accountId);
} catch (e) {
  if ((e as { errorCode?: string }).errorCode === 'NO_DATA') {
    // re-fetch SimpleFin accounts / prompt bank re-auth, or skip gracefully
  } else throw e;
}

Prevention

When it happens

Trigger: The SimpleFin /transactions response body is an empty object, e.g. the requested acctId(s) are no longer in the SimpleFin account set or the startDate filter excludes everything.

Common situations: SimpleFin account was removed or renamed on the provider side; syncing with a malformed/stale account id; SimpleFin returned a 200 with no data after a bank re-auth was required; first-time sync before the bank finished aggregating.

Related errors


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