actualbudget/actual · error

User or file not found

Error message

User or file not found

What it means

addUserAccess checks that both the user and the file actually exist (via getUserById and getFileById) and throws 'User or file not found' if either lookup returns null. This keeps the user_access join table free of dangling references. The insert never executes when this fires.

Source

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

  return (
    getAccountDb().first(
      `SELECT 1 as granted
       FROM files
       WHERE files.id = ? and (files.owner = ?)`,
      [fileId, userId],
    ) || { granted: 0 }
  );
}

export function addUserAccess(userId, fileId) {
  if (!userId || !fileId) {
    throw new Error('Invalid parameters');
  }
  try {
    const userExists = getUserById(userId);
    const fileExists = getFileById(fileId);
    if (!userExists || !fileExists) {
      throw new Error('User or file not found');
    }
    getAccountDb().mutate(
      'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)',
      [userId, fileId],
    );
  } catch (error) {
    if (error.message.includes('UNIQUE constraint')) {
      throw new Error('Access already exists');
    }
    throw new Error(`Failed to add user access: ${error.message}`);
  }
}

export function deleteUserAccessByFileId(userIds, fileId) {
  if (!Array.isArray(userIds) || userIds.length === 0) {
    throw new Error('The provided userIds must be a non-empty array.');
  }

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Call getUserById(userId) and getFileById(fileId) first; create the missing user or verify the file exists before sharing
  2. Confirm you are passing ids (users.id / files.id), not usernames or display names
  3. Refresh the user/file list from the server — stale client data often references deleted rows
  4. Verify both ids belong to the same account database / server instance

Example fix

// before
await addUserAccess(targetUserId, fileId);
// after
if (!getUserById(targetUserId)) {
  return res.status(404).json({ error: `User ${targetUserId} does not exist` });
}
await addUserAccess(targetUserId, fileId);
Defensive patterns

Strategy: validation

Validate before calling

import { getUserById, getFileById } from './services/user-service';
if (!getUserById(userId)) throw new Error(`User ${userId} not found`);
if (!getFileById(fileId)) throw new Error(`File ${fileId} not found`);

Type guard

function canGrantAccess(userId, fileId) {
  return getUserById(userId) !== null && getFileById(fileId) !== null;
}

Try / catch

try {
  addUserAccess(userId, fileId);
} catch (e) {
  if (e.message.includes('User or file not found')) {
    return res.status(404).json({ error: 'Unknown user or file' });
  }
  throw e;
}

Prevention

When it happens

Trigger: Sharing a file with a userId that has no row in users (deleted or never-provisioned user), or a fileId with no row in files (deleted file or wrong database). One of the two being valid is not enough — either missing triggers this.

Common situations: Sharing to a user whose account was disabled-and-deleted by another admin; typing a username where an id is expected; referencing a file id from a restored/rolled-back database; cross-environment testing with ids from a different server instance.

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/6a64ce755fefb761. Report an issue: GitHub.