actualbudget/actual · error

Failed to transfer files: ${error.message}

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.

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)

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.