actualbudget/actual · error

Failed to transfer files

Error message

Failed to transfer files: ${error.message}

What it means

transferAllFilesFromUser catches any error raised inside its try block — including the 'Invalid user IDs' and 'New owner not found' errors it throws itself — and rethrows it wrapped as `Failed to transfer files: <original message>`. This wrapper indicates the ownership-transfer transaction did not complete. The original cause is preserved in the message suffix.

Solutions

  1. Read the full message after the colon to identify the underlying cause ('Invalid user IDs', 'New owner not found', or a SQLite error)
  2. If the cause is 'New owner not found', provision/verify the owner user before the transfer call
  3. If the cause is a SQLite error, check database file permissions, disk space, and that no other process holds a write lock
  4. Call transferAllFilesFromUser inside your own try/catch and log error.message fully rather than truncating it

Example fix

// before
await transferAllFilesFromUser(ownerId, oldUserId); // opaque wrapped error
// after
try {
  await transferAllFilesFromUser(ownerId, oldUserId);
} catch (e) {
  logger.error('transfer failed, cause:', e.message); // shows 'Failed to transfer files: New owner not found'
  if (e.message.includes('New owner not found')) {
    await provisionUser(ownerId);
    await transferAllFilesFromUser(ownerId, oldUserId);
  }
}
Defensive patterns

Strategy: try-catch

Validate before calling

function canTransfer(ownerId, oldUserId) {
  return Boolean(ownerId) && Boolean(oldUserId) && getUserById(ownerId) !== null;
}
if (!canTransfer(ownerId, oldUserId)) throw new Error('precheck failed');

Type guard

function isTransferable(ownerId, oldUserId) {
  return typeof ownerId === 'string' && ownerId.length > 0 &&
         typeof oldUserId === 'string' && oldUserId.length > 0;
}

Try / catch

try {
  transferAllFilesFromUser(ownerId, oldUserId);
} catch (e) {
  const cause = e.message.replace('Failed to transfer files: ', '');
  logger.error({ cause }, 'file transfer failed');
  if (cause.includes('New owner not found')) await provisionAndRetry(ownerId, oldUserId);
  else throw e;
}

Prevention

When it happens

Trigger: Any failure inside the function: falsy ownerId/oldUserId ('Invalid user IDs'), missing owner ('New owner not found'), or an underlying SQLite error during the UPDATE files statement (locked db, disk error, schema mismatch).

Common situations: Seeing only the generic prefix in logs and missing the real cause; OpenID finalize handlers hitting 'New owner not found' wrapped here; a locked or read-only account database producing SQLite errors wrapped here.

Related errors


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

Appendix: source

Thrown at packages/sync-server/src/services/user-service.ts:125

}

export function transferAllFilesFromUser(ownerId, oldUserId) {
  if (!ownerId || !oldUserId) {
    throw new Error('Invalid user IDs');
  }
  try {
    getAccountDb().transaction(() => {
      const ownerExists = getUserById(ownerId);
      if (!ownerExists) {
        throw new Error('New owner not found');
      }
      getAccountDb().mutate('UPDATE files set owner = ? WHERE owner = ?', [
        ownerId,
        oldUserId,
      ]);
    });
  } catch (error) {
    throw new Error(`Failed to transfer files: ${error.message}`);
  }
}

export function updateFileOwner(ownerId, fileId) {
  if (!ownerId || !fileId) {
    throw new Error('Invalid parameters');
  }
  try {
    const result = getAccountDb().mutate(
      'UPDATE files set owner = ? WHERE id = ?',
      [ownerId, fileId],
    );
    if (result.changes === 0) {
      throw new Error('File not found');
    }
  } catch (error) {
    throw new Error(`Failed to update file owner: ${error.message}`);
  }

View on GitHub (pinned to d4334cb6e6)