actualbudget/actual · error

user-already-exists

user-already-exists

Error message

User ${userName} already exists

What it means

The POST /users admin handler checks `UserService.getUserByUsername(userName)` before creating an account. If a user with that username already exists, it responds 400 with reason 'user-already-exists' instead of creating a duplicate. Usernames are unique in the sync-server's user table.

Source

Thrown at packages/sync-server/src/app-admin.js:80

      reason: `${!userName ? 'user-cant-be-empty' : 'role-cant-be-empty'}`,
      details: `${!userName ? 'Username' : 'Role'} cannot be empty`,
    });
    return;
  }

  const roleIdFromDb = UserService.validateRole(role);
  if (!roleIdFromDb) {
    res.status(400).send({
      status: 'error',
      reason: 'role-does-not-exists',
      details: 'Selected role does not exist',
    });
    return;
  }

  const userIdInDb = UserService.getUserByUsername(userName);
  if (userIdInDb) {
    res.status(400).send({
      status: 'error',
      reason: 'user-already-exists',
      details: `User ${userName} already exists`,
    });
    return;
  }

  const userId = uuidv4();
  UserService.insertUser(
    userId,
    userName,
    displayName || null,
    enabled ? 1 : 0,
  );

  res.status(200).send({ status: 'ok', data: { id: userId } });
});

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Pick a different username, or look up the existing user's id via the admin API and reuse it.
  2. Make provisioning scripts idempotent: query the user first and skip creation if it exists.
  3. If the old user should not exist, delete it via DELETE /users then retry creation.
  4. Disable the submit button / deduplicate requests in the UI to prevent double submission.

Example fix

// before
for (const u of users) await createUsers({ userName: u.name, password: u.password });
// after
for (const u of users) {
  const existing = await getUserByUsername(u.name); // skip if found
  if (!existing) await createUsers({ userName: u.name, password: u.password });
}
Defensive patterns

Strategy: validation

Validate before calling

const existing = await getUserByUsername(userName); // admin API lookup
if (existing) {
  console.log(`Skipping ${userName}: already exists (id=${existing.id})`);
} else {
  await createUser({ userName, password, role });
}

Try / catch

try {
  await createUser({ userName, password, role });
} catch (e) {
  if (e.status === 400 && e.reason === 'user-already-exists') {
    // treat as success in idempotent provisioning
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: POST /users (admin session) where the userName exactly matches an existing row in the users table, e.g. re-running a provisioning script, double-submitting a signup form, or after a prior partially-failed request that still created the user.

Common situations: Idempotency-unaware automation that re-provisions users on each deploy; users re-registering with a previously taken name; recovery from a crash between user creation and response delivery; migrations importing a user dump twice.

Related errors


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