{"record":{"id":"a475bf4e2c121513","repo":"moeru-ai/airi","slug":"failed-to-remove-character","errorCode":null,"errorMessage":"Failed to remove character","messagePattern":"Failed to remove character","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/services/characters.ts","lineNumber":210,"sourceCode":"  async function updateRemote(client: CharactersRemoteClient, id: string, payload: UpdateCharacterPayload, options?: CharacterServiceOptions): Promise<Character> {\n    options?.abortSignal?.throwIfAborted()\n    const res = await client.api.v1.characters[':id'].$patch({\n      param: { id },\n      json: payload,\n    }, requestOptions(options))\n    if (!res.ok)\n      throw new Error('Failed to update character')\n\n    const data = await res.json()\n    options?.abortSignal?.throwIfAborted()\n    return parse(data)\n  }\n\n  async function removeRemote(client: CharactersRemoteClient, id: string, options?: CharacterServiceOptions): Promise<void> {\n    options?.abortSignal?.throwIfAborted()\n    const res = await client.api.v1.characters[':id'].$delete({ param: { id } }, requestOptions(options))\n    if (!res.ok)\n      throw new Error('Failed to remove character')\n    options?.abortSignal?.throwIfAborted()\n  }\n\n  async function likeRemote(client: CharactersRemoteClient, id: string, options?: CharacterServiceOptions): Promise<Character> {\n    options?.abortSignal?.throwIfAborted()\n    const res = await client.api.v1.characters[':id'].like.$post({ param: { id } }, requestOptions(options))\n    if (!res.ok)\n      throw new Error('Failed to like character')\n\n    const data = await res.json()\n    options?.abortSignal?.throwIfAborted()\n    return parse(data)\n  }\n\n  async function bookmarkRemote(client: CharactersRemoteClient, id: string, options?: CharacterServiceOptions): Promise<Character> {\n    options?.abortSignal?.throwIfAborted()\n    const res = await client.api.v1.characters[':id'].bookmark.$post({ param: { id } }, requestOptions(options))\n    if (!res.ok)","sourceCodeStart":192,"sourceCodeEnd":228,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/packages/stage-ui/src/services/characters.ts#L192-L228","documentation":"Thrown by CharactersService.removeRemote when DELETE /v1/characters/:id returns non-ok. Unlike the other operations, removeRemote does not parse JSON on success (the typed return is { ok: boolean }); it only checks ok and re-checks the abort signal. The message discards the status.","triggerScenarios":"client.api.v1.characters[':id'].$delete({ param: { id } }) resolves with ok=false. Typical: 401/403 (not owner / not authenticated), 404 (already deleted), 500 (server). Optimistic UI may have already removed the local copy, masking a server-side failure.","commonSituations":"Double delete (user clicks delete twice, or the character was already gone); permission mismatch; session expired; transient server error during deletion.","solutions":["Treat 404 on delete as success if the local copy is already gone (idempotent delete).","Re-authenticate on 401/403 before retrying.","Confirm ownership before showing the delete control.","Include res.status in the message to allow 404-tolerant handling upstream."],"exampleFix":"// before\nif (!res.ok)\n  throw new Error('Failed to remove character')\n\n// after: tolerate already-deleted\nif (!res.ok && res.status !== 404)\n  throw new Error(`Failed to remove character ${id} (status ${res.status})`)","handlingStrategy":"try-catch","validationCode":"// Deletion is idempotent; nothing required beyond a valid id and auth.\nfunction isValidCharacterId(id: string): boolean {\n  return typeof id === 'string' && id.length > 0\n}","typeGuard":null,"tryCatchPattern":"try {\n  await removeRemote(client, id)\n}\ncatch (error) {\n  // Patch service to expose status; tolerate 404 since the character is already gone.\n  // On 401/403 re-authenticate and retry once.\n  throw error\n}","preventionTips":["Treat 404 on delete as success when the local copy is already removed (idempotent).","Embed res.status in the service error so callers can tolerate 404.","Guard against double-delete by disabling the control after the first click."],"tags":["network","http","characters","api","auth","not-found"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}