actualbudget/actual · warning
Access already exists
Error message
Access already exists
What it means
addUserAccess inserts into user_access, which has a UNIQUE constraint on (user_id, file_id). When the INSERT violates it, SQLite raises a UNIQUE constraint error which this function detects and rethrows as 'Access already exists' — meaning the user already has access to the file. It is an idempotency signal, not corruption.
Source
Thrown at packages/sync-server/src/services/user-service.ts:199
}
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.');
}
const CHUNK_SIZE = 999;
let totalChanges = 0;
try {
getAccountDb().transaction(() => {
for (let i = 0; i < userIds.length; i += CHUNK_SIZE) {
const chunk = userIds.slice(i, i + CHUNK_SIZE);
const placeholders = chunk.map(() => '?').join(',');View on GitHub (pinned to d4334cb6e6)
Solutions
- Check existing access first (getUserAccess or countUserAccess) and skip the insert if it already returns a row
- Treat this error as success/no-op in callers that only need the access to exist (idempotent handling)
- Debounce/disable the share action in the UI after the first submission
- Use INSERT OR IGNORE semantics at the call site pattern: check-then-insert or catch-and-continue
Example fix
// before
await addUserAccess(userId, fileId); // throws on retry
// after
const existing = countUserAccess(fileId, userId);
if (!existing) {
await addUserAccess(userId, fileId);
} Defensive patterns
Strategy: validation
Validate before calling
import { countUserAccess } from './services/user-service';
async function grantAccessOnce(userId, fileId) {
if (countUserAccess(fileId, userId) > 0) return; // already shared
await addUserAccess(userId, fileId);
} Type guard
function hasAccess(userId, fileId) {
return countUserAccess(fileId, userId) > 0;
} Try / catch
try {
addUserAccess(userId, fileId);
} catch (e) {
if (e.message.includes('Access already exists')) {
return; // idempotent success
}
throw e;
} Prevention
- Check countUserAccess before inserting
- Treat duplicates as success in idempotent callers
- Debounce/disable share buttons after first click
- Guard concurrent shares with a per-(user,file) lock or queue
When it happens
Trigger: Calling addUserAccess twice with the same (userId, fileId) pair — double-clicking a share button, retrying a request that actually succeeded, replaying a share operation, or concurrent requests sharing the same file with the same user simultaneously.
Common situations: Front-end share forms submitting twice without disabling the button; automation scripts re-running without checking current access; race conditions between two admins sharing the same file at once; re-running a migration/import script.
Related errors
- user-already-have-access
- A category with the name "${category.name}" already exists.
- user-already-exists
- No id returned from download.
- response.reason || response.error_code
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/4f9ac9026fb3a50a.
Report an issue: GitHub.