moeru-ai/airi · error · Error

Failed to fetch character

Error message

Failed to fetch character

What it means

Thrown by CharactersService.fetchRemoteById when GET /v1/characters/:id returns non-ok. The service parses a single character via the valibot CharacterWithRelationsSchema on success; on failure it aborts before parsing. Like the list fetch, the status code is discarded in the message.

Source

Thrown at packages/stage-ui/src/services/characters.ts:174

  async function fetchRemote(client: CharactersRemoteClient, params: { all?: boolean }, options?: CharacterServiceOptions): Promise<Character[]> {
    options?.abortSignal?.throwIfAborted()
    const res = await client.api.v1.characters.$get({
      query: { all: String(params.all ?? false) },
    }, requestOptions(options))
    if (!res.ok)
      throw new Error('Failed to fetch characters')

    const data = await res.json()
    options?.abortSignal?.throwIfAborted()
    return data.map((item: unknown) => parse(item))
  }

  async function fetchRemoteById(client: CharactersRemoteClient, id: string, options?: CharacterServiceOptions): Promise<Character> {
    options?.abortSignal?.throwIfAborted()
    const res = await client.api.v1.characters[':id'].$get({ param: { id } }, requestOptions(options))
    if (!res.ok)
      throw new Error('Failed to fetch character')

    const data = await res.json()
    options?.abortSignal?.throwIfAborted()
    return parse(data)
  }

  async function createRemote(client: CharactersRemoteClient, payload: CreateCharacterPayload, options?: CharacterServiceOptions): Promise<Character> {
    options?.abortSignal?.throwIfAborted()
    const res = await client.api.v1.characters.$post({ json: payload }, requestOptions(options))
    if (!res.ok)
      throw new Error('Failed to create character')

    const data = await res.json()
    options?.abortSignal?.throwIfAborted()
    return parse(data)
  }

  async function updateRemote(client: CharactersRemoteClient, id: string, payload: UpdateCharacterPayload, options?: CharacterServiceOptions): Promise<Character> {

View on GitHub (pinned to 27111382b4)

Solutions

  1. Handle 404 distinctly: remove the character from the local store and notify the user it no longer exists.
  2. Confirm the id is well-formed and exists (e.g. was returned by a prior list fetch).
  3. Include res.status in the error message to enable status-based handling upstream.
  4. Retry once on 5xx before surfacing the failure.

Example fix

// before
if (!res.ok)
  throw new Error('Failed to fetch character')

// after: branch on status
if (!res.ok) {
  if (res.status === 404)
    throw new NotFoundError(`Character ${id} not found`)
  throw new Error(`Failed to fetch character ${id} (status ${res.status})`)
}
Defensive patterns

Strategy: try-catch

Validate before calling

function isValidCharacterId(id: string): boolean {
  return typeof id === 'string' && id.length > 0
}

// before fetching
if (!isValidCharacterId(id))
  throw new Error(`Invalid character id: ${id}`)
await fetchRemoteById(client, id)

Try / catch

try {
  return await fetchRemoteById(client, id)
}
catch (error) {
  // Patch service to expose status; treat likely-404 as 'removed' and update local store.
  if (String(error).includes('Failed to fetch character')) {
    removeCharacterLocally(id)
  }
  throw error
}

Prevention

When it happens

Trigger: client.api.v1.characters[':id'].$get({ param: { id } }) resolves with ok=false. Most common: 404 (character was deleted or id is wrong), 401/403 (auth), 500 (server). Also when a just-deleted character is still referenced in the UI and a refetch is attempted.

Common situations: Stale character id in a deep link or store; concurrent deletion by another client; permission to list but not to view a specific character; transient server error.

Related errors


AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12). Data as JSON: /api/errors/849b3a9826b24746. Report an issue: GitHub.