{"record":{"id":"eb96749a594dea63","repo":"tldraw/tldraw","slug":"e-instanceof-error-e-message-string-e","errorCode":null,"errorMessage":"e instanceof Error ? e.message : String(e)","messagePattern":"e instanceof Error \\? e\\.message : String\\(e\\)","errorType":"http","errorClass":"StatusError","httpStatus":400,"severity":"error","filePath":"apps/dotcom/sync-worker/src/adminRoutes.ts","lineNumber":271,"sourceCode":"\t\tif (enabled !== undefined) update.enabled = enabled\n\t\tif (percentage !== undefined) update.percentage = percentage\n\n\t\tawait setFeatureFlag(env, flag as FeatureFlagKey, update)\n\t\treturn json({ success: true, flag, ...update })\n\t})\n\t.get('/app/admin/mcp-friends-and-family', async (_req, env) => {\n\t\treturn json({ entries: await getFriendsAndFamilyList(env) })\n\t})\n\t.post('/app/admin/mcp-friends-and-family', async (req, env) => {\n\t\tconst body: any = await req.json()\n\n\t\t// Parsing before resolving means a typo is rejected at the point someone can still fix it,\n\t\t// rather than sitting in the list looking like it grants access while matching nothing.\n\t\tlet emails: string[]\n\t\ttry {\n\t\t\temails = parseFriendsAndFamilyEmails(body?.entries)\n\t\t} catch (e) {\n\t\t\tthrow new StatusError(400, e instanceof Error ? e.message : String(e))\n\t\t}\n\n\t\tconst entries = await resolveFriendsAndFamilyUsers(env, emails)\n\t\tawait setFriendsAndFamilyList(env, entries)\n\t\treturn json({ success: true, entries })\n\t})\n\t.post('/app/admin/create_legacy_file', async (_res, env) => {\n\t\tconst slug = uniqueId()\n\t\tawait getRoomDurableObject(env, slug).__admin__createLegacyRoom(slug)\n\t\treturn json({ slug })\n\t})\n\t.post('/app/admin/hard_delete_file/:fileId', async (res, env) => {\n\t\tconst fileId = res.params.fileId\n\t\tassert(typeof fileId === 'string', 'fileId is required')\n\n\t\tconst pg = createPostgresConnectionPool(env, '/app/admin/hard_delete_file')\n\t\tconst file = await pg.selectFrom('file').where('id', '=', fileId).selectAll().executeTakeFirst()\n\t\tif (!file) {","sourceCodeStart":253,"sourceCodeEnd":289,"githubUrl":"https://github.com/tldraw/tldraw/blob/b31086b44731a7d1d9d46be4163ac8ef7417321d/apps/dotcom/sync-worker/src/adminRoutes.ts#L253-L289","documentation":"Thrown by POST /app/admin/mcp-friends-and-family when parseFriendsAndFamilyEmails throws during parsing. The catch wraps the parser's error message into a StatusError(400) so the caller sees exactly which entry was invalid. The message is dynamic — it reflects whatever the parser rejected (bad format, duplicate, empty entry).","triggerScenarios":"POST /app/admin/mcp-friends-and-family with body.entries containing malformed emails, non-string entries, duplicates, or empty strings. The parser validates format before any DB lookup.","commonSituations":"Pasting a comma-separated blob that includes 'Name <email>' format; entries with spaces or semicolons; mixed types in the array (numbers, objects).","solutions":["Inspect the wrapped message — it names the specific invalid entry.","Normalize entries to plain email strings before posting; strip names, angle brackets, and whitespace.","Validate each entry with a standard email regex client-side before submission."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"function parseEmails(entries: unknown): string[] {\n  const arr = Array.isArray(entries) ? entries : [entries]\n  return arr.map((e) => {\n    if (typeof e !== 'string') throw new Error(`Entry is not a string: ${JSON.stringify(e)}`)\n    const trimmed = e.trim()\n    if (!/^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(trimmed)) throw new Error(`Invalid email: ${trimmed}`)\n    return trimmed.toLowerCase()\n  })\n}","typeGuard":"function isEmail(value: unknown): value is string {\n  return typeof value === 'string' && /^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(value.trim())\n}","tryCatchPattern":"try {\n  await fetch('/app/admin/mcp-friends-and-family', { method: 'POST', body: JSON.stringify({ entries }) })\n} catch (e) {\n  // e.message contains the specific invalid entry from the parser\n}","preventionTips":["Strip 'Name <email>' formatting down to the bare email before posting.","Validate each entry with an email regex client-side.","Inspect the wrapped parser message for the offending entry."],"tags":["admin","validation","friends-and-family","mcp"],"backgroundTag":null,"analyzedSha":"b31086b44731a7d1d9d46be4163ac8ef7417321d","analyzedAt":"2026-08-12T17:05:39.947Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}