moeru-ai/airi · error
Character not found
Error message
Character not found
What it means
The character update service throws this when it cannot confirm any live (non-soft-deleted) character row for the given id. It is raised inside the update transaction after the UPDATE matched no row (or only capabilities were supplied with no field updates), and the fallback SELECT also found nothing. In other words, the target character either never existed or was soft-deleted via `deletedAt`.
Source
Thrown at server/apps/api/src/services/domain/characters.ts:243
await tx.insert(schema.characterCapabilities).values(
capabilities.map(c => ({ ...c, characterId: id })),
)
}
}
if (updatedChar) {
return updatedChar
}
const fallback = await tx.query.character.findFirst({
where: and(
eq(schema.character.id, id),
isNull(schema.character.deletedAt),
),
})
if (!fallback)
throw new Error('Character not found')
return fallback
})
logger.withFields({ id }).log('Updated character')
return result
},
async delete(id: string) {
const result = await db.update(schema.character)
.set({ deletedAt: new Date() })
.where(and(
eq(schema.character.id, id),
isNull(schema.character.deletedAt),
))
.returning()
if (result.length > 0) {
logger.withFields({ id }).log('Deleted character')View on GitHub (pinned to f679616c34)
Solutions
- Verify the character id exists and is not soft-deleted: query the character table for `id = <id> AND deleted_at IS NULL` before updating.
- Re-fetch the character list from the API to refresh any stale client-side id.
- If the row was soft-deleted intentionally, create a new character instead of updating the deleted one.
- Callers that treat not-found as expected should catch this and map it to a 404 ApiError rather than a 500.
Example fix
// before
await characterService.update('dead-id', { version: '2' })
// after
const existing = await db.query.character.findFirst({ where: and(eq(schema.character.id, id), isNull(schema.character.deletedAt)) })
if (!existing) throw new ApiError(404, 'CHARACTER_NOT_FOUND', 'Character not found')
await characterService.update(id, { version: '2' }) Defensive patterns
Strategy: try-catch
Validate before calling
const exists = await db.query.character.findFirst({ where: and(eq(schema.character.id, id), isNull(schema.character.deletedAt)) })
if (!exists) throw new ApiError(404, 'CHARACTER_NOT_FOUND', 'Character not found') Type guard
function isLiveCharacter(c: { deletedAt: Date | null } | undefined | null): c is { deletedAt: null } { return !!c && c.deletedAt === null } Try / catch
try {
const character = await characterService.update(id, data)
} catch (error) {
if (error instanceof Error && error.message === 'Character not found') {
throw new ApiError(404, 'CHARACTER_NOT_FOUND', 'Character not found')
}
throw error
} Prevention
- Always resolve the character from a fresh server query instead of trusting cached client ids.
- Check `deletedAt IS NULL` before issuing updates to soft-deletable rows.
- Map domain not-found errors to 404 at the route layer so clients see 404, not 500.
- Handle soft-deleted characters by recreating rather than updating.
When it happens
Trigger: Calling the `update` domain service with an id that does not exist in `schema.character`, with an id whose row has `deletedAt` set (soft-deleted), or calling update with an empty `characterData` payload (only `capabilities`) for a nonexistent/deleted id so only the fallback SELECT runs.
Common situations: Client caches a character id that was soft-deleted on another instance; a stale or fabricated id passed to the PATCH/PUT route; concurrent delete racing an update; passing a capability-only payload for an id that was never created.
Understand the failure class
Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.
AI-assisted analysis of moeru-ai/airi@f679616c34 (2026-09-08).
Data as JSON: /api/errors/6ada6760dd3886c2.
Report an issue: GitHub.