actualbudget/actual · error · Error

getSyncError(result.error.reason, localBudget.id, result.err

Error message

getSyncError(result.error.reason, localBudget.id, result.error.meta)

What it means

Thrown by api/download-budget in packages/loot-core/src/server/api.ts after the local budget is loaded and handlers['sync-budget']() returns an error. The reason plus meta are formatted by getSyncError() into messages for out-of-sync-migrations/data, invalid-schema (newer DB schema), budget-not-found, or clock-drift. The original reason code is preserved on the thrown error.

Source

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

    const result = await handlers['key-test']({
      cloudFileId: remoteBudget ? remoteBudget.fileId : localBudget.cloudFileId,
      password,
    });
    if (result.error) {
      throw withErrorCode(
        new Error(getTestKeyError(result.error)),
        result.error.reason,
      );
    }
  }

  // Sync the local budget file
  if (localBudget) {
    await handlers['load-budget']({ id: localBudget.id });
    const result = await handlers['sync-budget']();
    if (result.error) {
      throw withErrorCode(
        new Error(
          getSyncError(result.error.reason, localBudget.id, result.error.meta),
        ),
        result.error.reason,
      );
    }
    return;
  }

  // Download the remote file (no need to perform a sync as the file will already be up-to-date)
  const result = await handlers['download-budget']({
    cloudFileId: remoteBudget.fileId,
  });
  if (result.error) {
    logger.log('Full error details', result.error);
    throw withErrorCode(
      new Error(getDownloadError(result.error)),
      result.error.reason,

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Update Actual to the latest version on all devices (schema/migration mismatches)
  2. Sync system clock via NTP if reason is clock-drift
  3. If out-of-sync-data cannot resolve, back up and delete the stale local file, then re-download
  4. Read error.code for the precise reason and branch accordingly

Example fix

// before
await api.downloadBudget(syncId);
// after
try {
  await api.downloadBudget(syncId);
} catch (e) {
  if (e.code === 'clock-drift') { await syncClock(); await api.downloadBudget(syncId); }
  else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Clock sanity before syncing
const skewMin = Math.abs(Date.now() - Date.parse(await fetchServerTime())) / 60000;
if (skewMin > 5) console.warn('Clock drift detected; sync may fail with clock-drift');

Type guard

function isSyncError(e: unknown): e is { code: string } {
  return typeof e === 'object' && e !== null && typeof (e as any).code === 'string';
}

Try / catch

try {
  await api.downloadBudget(syncId);
} catch (e) {
  if (isSyncError(e) && e.code === 'clock-drift') { await syncNtp(); await api.downloadBudget(syncId); }
  else if (isSyncError(e) && e.code === 'out-of-sync-data') await updateActualBeforeRetry();
  else throw e;
}

Prevention

When it happens

Trigger: downloadBudget(syncId) with an existing local budget whose initial sync fails: local data out of sync with migrations, schema newer than app, device clock drifted from the sync server, or the local file's group id no longer exists on the server.

Common situations: Upgraded Actual partially (old app, new budget); Docker/VM clock skew; budget restored from backup into a mismatched app version; server wiped while a stale local file remains.

Related errors


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