{"record":{"id":"6ada6760dd3886c2","repo":"moeru-ai/airi","slug":"character-not-found","errorCode":null,"errorMessage":"Character not found","messagePattern":"Character not found","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/apps/api/src/services/domain/characters.ts","lineNumber":243,"sourceCode":"\n            await tx.insert(schema.characterCapabilities).values(\n              capabilities.map(c => ({ ...c, characterId: id })),\n            )\n          }\n        }\n\n        if (updatedChar) {\n          return updatedChar\n        }\n\n        const fallback = await tx.query.character.findFirst({\n          where: and(\n            eq(schema.character.id, id),\n            isNull(schema.character.deletedAt),\n          ),\n        })\n        if (!fallback)\n          throw new Error('Character not found')\n        return fallback\n      })\n\n      logger.withFields({ id }).log('Updated character')\n      return result\n    },\n\n    async delete(id: string) {\n      const result = await db.update(schema.character)\n        .set({ deletedAt: new Date() })\n        .where(and(\n          eq(schema.character.id, id),\n          isNull(schema.character.deletedAt),\n        ))\n        .returning()\n\n      if (result.length > 0) {\n        logger.withFields({ id }).log('Deleted character')","sourceCodeStart":225,"sourceCodeEnd":261,"githubUrl":"https://github.com/moeru-ai/airi/blob/f679616c34f1cf6d282c8d64264242af944b3fed/server/apps/api/src/services/domain/characters.ts#L225-L261","documentation":"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`.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait characterService.update('dead-id', { version: '2' })\n\n// after\nconst existing = await db.query.character.findFirst({ where: and(eq(schema.character.id, id), isNull(schema.character.deletedAt)) })\nif (!existing) throw new ApiError(404, 'CHARACTER_NOT_FOUND', 'Character not found')\nawait characterService.update(id, { version: '2' })","handlingStrategy":"try-catch","validationCode":"const exists = await db.query.character.findFirst({ where: and(eq(schema.character.id, id), isNull(schema.character.deletedAt)) })\nif (!exists) throw new ApiError(404, 'CHARACTER_NOT_FOUND', 'Character not found')","typeGuard":"function isLiveCharacter(c: { deletedAt: Date | null } | undefined | null): c is { deletedAt: null } { return !!c && c.deletedAt === null }","tryCatchPattern":"try {\n  const character = await characterService.update(id, data)\n} catch (error) {\n  if (error instanceof Error && error.message === 'Character not found') {\n    throw new ApiError(404, 'CHARACTER_NOT_FOUND', 'Character not found')\n  }\n  throw error\n}","preventionTips":["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."],"tags":["database","not-found","soft-delete","drizzle"],"backgroundTag":"record-not-found","analyzedSha":"f679616c34f1cf6d282c8d64264242af944b3fed","analyzedAt":"2026-09-08T13:18:15.005Z","contentChangedAt":"2026-09-08T13:18:15.005Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}