actualbudget/actual · error

New owner not found

Error message

New owner not found

What it means

transferAllFilesFromUser wraps all file-reassignment work in a database transaction and first verifies the target owner exists via getUserById. If no user row matches the supplied ownerId, it throws 'New owner not found' to abort the transfer before any files are reassigned. The transaction rolls back, so no partial ownership changes occur.

Source

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

export function deleteUserAccess(userId) {
  try {
    return getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [
      userId,
    ]).changes;
  } catch (error) {
    throw new Error(`Failed to delete user access: ${error.message}`);
  }
}

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 = ?',

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Verify the ownerId exists before calling: run getUserById(ownerId) and create/insert the user if null
  2. Check you are passing the user's id (users.id), not the userName or displayName
  3. Confirm the sync server is pointed at the same account database that contains the owner user (check the correct server/data dir)
  4. If the owner was deleted, recreate the user or pick a different existing owner id

Example fix

// before
await transferAllFilesFromUser(subFromToken, oldUserId);
// after
const ownerId = getUserByUsername(subFromToken);
if (!ownerId) throw new Error(`Owner ${subFromToken} not provisioned yet`);
await transferAllFilesFromUser(ownerId, oldUserId);
Defensive patterns

Strategy: validation

Validate before calling

import { getUserById } from './services/user-service';
function assertOwnerExists(ownerId) {
  if (!getUserById(ownerId)) {
    throw new Error(`Cannot transfer: owner ${ownerId} does not exist`);
  }
}
assertOwnerExists(ownerId);

Type guard

function isExistingUser(userId) {
  return typeof userId === 'string' && userId.length > 0 && getUserById(userId) !== null;
}

Try / catch

try {
  transferAllFilesFromUser(ownerId, oldUserId);
} catch (e) {
  if (e.message.includes('New owner not found')) {
    logger.error(`Owner ${ownerId} missing; provision it before transferring`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling transferAllFilesFromUser(ownerId, oldUserId) with an ownerId that has no row in the users table — e.g. a deleted or never-created user id, a stale id from a different database, or a caller (like loginWithOpenIdFinalize) resolving the owner from a token/sub claim that was not yet inserted as a user.

Common situations: OpenID login flows where the identity provider's subject does not match any local user record; passing a userName instead of a user id; referencing a user deleted by another admin between fetching and transferring; pointing at the wrong account.sqlite database.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


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