{"record":{"id":"0371788461d2be43","repo":"slopus/happy","slug":"username-is-already-taken","errorCode":null,"errorMessage":"Username is already taken","messagePattern":"Username is already taken","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/happy-server/sources/app/social/usernameUpdate.ts","lineNumber":18,"sourceCode":"import { db } from \"@/storage/db\";\nimport { Context } from \"@/context\";\nimport { allocateUserSeq } from \"@/storage/seq\";\nimport { buildUpdateAccountUpdate, eventRouter } from \"@/app/events/eventRouter\";\nimport { randomKeyNaked } from \"@/utils/randomKeyNaked\";\n\nexport async function usernameUpdate(ctx: Context, username: string): Promise<void> {\n    const userId = ctx.uid;\n\n    // Check if username is already taken\n    const existingUser = await db.account.findFirst({\n        where: {\n            username: username,\n            NOT: { id: userId }\n        }\n    });\n    if (existingUser) { // Should never happen\n        throw new Error('Username is already taken');\n    }\n\n    // Update username\n    await db.account.update({\n        where: { id: userId },\n        data: { username: username }\n    });\n\n    // Send account update to all user connections\n    const updSeq = await allocateUserSeq(userId);\n    const updatePayload = buildUpdateAccountUpdate(userId, { username: username }, updSeq, randomKeyNaked(12));\n    eventRouter.emitUpdate({\n        userId, payload: updatePayload,\n        recipientFilter: { type: 'user-scoped-only' }\n    });\n}","sourceCodeStart":1,"sourceCodeEnd":34,"githubUrl":"https://github.com/slopus/happy/blob/b824cd0a4681d41af631a8e422a813873e4455b0/packages/happy-server/sources/app/social/usernameUpdate.ts#L1-L34","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Catch the error and surface 'username taken' to the user, prompting a different name.","Retry the availability check (SELECT) and the update in a transaction to narrow the race window.","Add/verify a unique constraint on Account.username so the DB is the final arbiter and handle P2002 from Prisma.","Re-check availability immediately before the update call after any user idle time (form open, payment, etc.)."],"exampleFix":"// before\nawait updateUsername(userId, username); // throws if taken\n// after\nif (await isUsernameAvailable(username, userId)) {\n  await updateUsername(userId, username);\n} else {\n  show('Username is already taken, choose another');\n}","handlingStrategy":"try-catch","validationCode":"const taken = await db.account.findFirst({\n  where: { username, NOT: { id: userId } }\n});\nif (taken) throw new Error('Username is already taken');","typeGuard":null,"tryCatchPattern":"try {\n  await api.updateUsername(userId, username);\n} catch (e) {\n  if (e.message === 'Username is already taken') {\n    showUsernameUnavailable(username);\n    return;\n  }\n  throw e;\n}","preventionTips":["Always run an availability check against the server right before submit (not when the form opens).","Add a DB unique constraint on username and also catch Prisma P2002.","Debounce availability checks as the user types to catch most conflicts early.","After any conflict, re-seed the form's availability state before retry."],"tags":["uniqueness","race-condition","validation","prisma"],"backgroundTag":"username-already-taken","analyzedSha":"b824cd0a4681d41af631a8e422a813873e4455b0","analyzedAt":"2026-08-31T23:12:36.205Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}