tldraw/tldraw · error · StatusError

e instanceof Error ? e.message : String(e)

Error message

e instanceof Error ? e.message : String(e)

What it means

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).

Source

Thrown at apps/dotcom/sync-worker/src/adminRoutes.ts:271

		if (enabled !== undefined) update.enabled = enabled
		if (percentage !== undefined) update.percentage = percentage

		await setFeatureFlag(env, flag as FeatureFlagKey, update)
		return json({ success: true, flag, ...update })
	})
	.get('/app/admin/mcp-friends-and-family', async (_req, env) => {
		return json({ entries: await getFriendsAndFamilyList(env) })
	})
	.post('/app/admin/mcp-friends-and-family', async (req, env) => {
		const body: any = await req.json()

		// Parsing before resolving means a typo is rejected at the point someone can still fix it,
		// rather than sitting in the list looking like it grants access while matching nothing.
		let emails: string[]
		try {
			emails = parseFriendsAndFamilyEmails(body?.entries)
		} catch (e) {
			throw new StatusError(400, e instanceof Error ? e.message : String(e))
		}

		const entries = await resolveFriendsAndFamilyUsers(env, emails)
		await setFriendsAndFamilyList(env, entries)
		return json({ success: true, entries })
	})
	.post('/app/admin/create_legacy_file', async (_res, env) => {
		const slug = uniqueId()
		await getRoomDurableObject(env, slug).__admin__createLegacyRoom(slug)
		return json({ slug })
	})
	.post('/app/admin/hard_delete_file/:fileId', async (res, env) => {
		const fileId = res.params.fileId
		assert(typeof fileId === 'string', 'fileId is required')

		const pg = createPostgresConnectionPool(env, '/app/admin/hard_delete_file')
		const file = await pg.selectFrom('file').where('id', '=', fileId).selectAll().executeTakeFirst()
		if (!file) {

View on GitHub (pinned to b31086b447)

Solutions

  1. Inspect the wrapped message — it names the specific invalid entry.
  2. Normalize entries to plain email strings before posting; strip names, angle brackets, and whitespace.
  3. Validate each entry with a standard email regex client-side before submission.
Defensive patterns

Strategy: validation

Validate before calling

function parseEmails(entries: unknown): string[] {
  const arr = Array.isArray(entries) ? entries : [entries]
  return arr.map((e) => {
    if (typeof e !== 'string') throw new Error(`Entry is not a string: ${JSON.stringify(e)}`)
    const trimmed = e.trim()
    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(trimmed)) throw new Error(`Invalid email: ${trimmed}`)
    return trimmed.toLowerCase()
  })
}

Type guard

function isEmail(value: unknown): value is string {
  return typeof value === 'string' && /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value.trim())
}

Try / catch

try {
  await fetch('/app/admin/mcp-friends-and-family', { method: 'POST', body: JSON.stringify({ entries }) })
} catch (e) {
  // e.message contains the specific invalid entry from the parser
}

Prevention

When it happens

Trigger: 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.

Common situations: Pasting a comma-separated blob that includes 'Name <email>' format; entries with spaces or semicolons; mixed types in the array (numbers, objects).

Related errors


AI-assisted analysis of tldraw/tldraw@b31086b447 (2026-08-12). Data as JSON: /api/errors/eb96749a594dea63. Report an issue: GitHub.