slopus/happy · error

Username is already taken

Error message

Username is already taken

What it means

usernameUpdate() checks that no OTHER account already owns the requested username before writing it. If a row with that username exists for a different user id, it throws 'Username is already taken'. The comment notes this 'should never happen' — a race or missing earlier uniqueness check let a duplicate through.

Source

Thrown at packages/happy-server/sources/app/social/usernameUpdate.ts:18

import { db } from "@/storage/db";
import { Context } from "@/context";
import { allocateUserSeq } from "@/storage/seq";
import { buildUpdateAccountUpdate, eventRouter } from "@/app/events/eventRouter";
import { randomKeyNaked } from "@/utils/randomKeyNaked";

export async function usernameUpdate(ctx: Context, username: string): Promise<void> {
    const userId = ctx.uid;

    // Check if username is already taken
    const existingUser = await db.account.findFirst({
        where: {
            username: username,
            NOT: { id: userId }
        }
    });
    if (existingUser) { // Should never happen
        throw new Error('Username is already taken');
    }

    // Update username
    await db.account.update({
        where: { id: userId },
        data: { username: username }
    });

    // Send account update to all user connections
    const updSeq = await allocateUserSeq(userId);
    const updatePayload = buildUpdateAccountUpdate(userId, { username: username }, updSeq, randomKeyNaked(12));
    eventRouter.emitUpdate({
        userId, payload: updatePayload,
        recipientFilter: { type: 'user-scoped-only' }
    });
}

View on GitHub (pinned to b824cd0a46)

Solutions

  1. Catch the error and surface 'username taken' to the user, prompting a different name.
  2. Retry the availability check (SELECT) and the update in a transaction to narrow the race window.
  3. Add/verify a unique constraint on Account.username so the DB is the final arbiter and handle P2002 from Prisma.
  4. Re-check availability immediately before the update call after any user idle time (form open, payment, etc.).

Example fix

// before
await updateUsername(userId, username); // throws if taken
// after
if (await isUsernameAvailable(username, userId)) {
  await updateUsername(userId, username);
} else {
  show('Username is already taken, choose another');
}
Defensive patterns

Strategy: try-catch

Validate before calling

const taken = await db.account.findFirst({
  where: { username, NOT: { id: userId } }
});
if (taken) throw new Error('Username is already taken');

Try / catch

try {
  await api.updateUsername(userId, username);
} catch (e) {
  if (e.message === 'Username is already taken') {
    showUsernameUnavailable(username);
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Two users concurrently requesting the same username and both passing the initial availability check; a caller invoking usernameUpdate for a username another account just claimed; retrying an update after a partial failure without re-checking availability.

Common situations: Username-set flows racing between devices; check-then-update without a DB-level unique constraint enforcement at this step; user resubmitting a form with a username taken in the meantime.

Related errors


AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31). Data as JSON: /api/errors/0371788461d2be43. Report an issue: GitHub.