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

  1. Verify the character id exists and is not soft-deleted: query the character table for `id = <id> AND deleted_at IS NULL` before updating.
  2. Re-fetch the character list from the API to refresh any stale client-side id.
  3. If the row was soft-deleted intentionally, create a new character instead of updating the deleted one.
  4. 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

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.