Brainby arc-labs/docs
Guide

Error handling

Catch the right error per client — BrainHttpError for HTTP, the BrainError hierarchy for the wire — decide retry vs surface, and know which of the nine categories are transient.

Prerequisites
  • A Brain client.
  • Familiarity with HTTP statuses 400/401/403/404/409/503.
What you'll build

A wrapper around Brain calls that classifies a failure as retry / surface / alert using the SDK's own taxonomy, without hard-coding a status list.

Two clients, two taxonomies

The error shape depends on which client you use. This guide is the task recipe; the exhaustive class lists and code tables live in the reference pages, so branch on the SDK's own helpers rather than re-implementing them.

Full references: API errors, TypeScript errors, Python errors.

Handling HTTP errors

Every BrainHttpClient failure is one BrainHttpError. status === 0 means no response arrived (transport or timeout). Branch on status/code; treat message as diagnostic text only.

import { BrainHttpError } from "@brain-db/sdk";

try {
  await brain.encode({ text });
} catch (e) {
  if (!(e instanceof BrainHttpError)) throw e;
  if (e.status === 0 || e.status === 503) {
    // transport/timeout or engine.unavailable — retriable (see below)
  } else if (e.status === 401 || e.status === 403) {
    // fix the credential or permissions — do not retry
  } else {
    // 400 / 404 / 409 / 502 — surface; the request or state is wrong
  }
  throw e;
}
from brain_db_sdk.http import BrainHttpError

try:
    brain.encode(text=text)
except BrainHttpError as e:
    if e.status in (0, 503):
        ...   # transport/timeout or engine.unavailable — retriable
    elif e.status in (401, 403):
        ...   # fix the credential or permissions — do not retry
    else:
        ...   # 400 / 404 / 409 / 502 — surface
    raise
use brain_db_sdk::http::EncodeInput;

if let Err(e) = brain
    .encode(&EncodeInput { text: text.to_string(), ..Default::default() })
    .await
{
    match e.status {
        0 | 503 => { /* transport/timeout or engine.unavailable — retriable (see below) */ }
        401 | 403 => { /* fix the credential or permissions — do not retry */ }
        _ => { /* 400 / 404 / 409 / 502 — surface; the request or state is wrong */ }
    }
    return Err(e.into());
}

The edge maps Brain's internal taxonomy onto a small stable set: bad_request (400), unauthorized (401), forbidden (403), not_found (404), conflict (409), engine.unavailable (503, retriable), engine.error (502). Only 503 and status 0 are worth a blind retry.

Handling wire errors

The wire client separates failure layers so you can react to each differently. Catch BrainError at the boundary; catch a subclass only where you have a specific recovery.

import { ServerError, ErrorCategoryWire, isActAsDenied, BrainError } from "@brain-db/sdk";

try {
  await client.encode(req);
} catch (e) {
  if (isActAsDenied(e)) throw e;                 // wrong-scoped credential — fix it
  if (e instanceof ServerError && e.category === ErrorCategoryWire.Validation) {
    throw e;                                      // bad input — surface to caller
  }
  if (e instanceof BrainError) {
    // ProtocolError / ConnectionClosed / BrainTimeout / ServerError — see below
  }
  throw e;
}
from brain_db_sdk import ServerError, ActAsDenied, BrainTimeout, BrainError

try:
    client.encode(req)
except ActAsDenied:
    raise                     # lacks can_act_as / namespace outside allowlist
except ServerError as e:
    print(f"{e.code:#06x}", e.category, e.retry_after_ms)
    raise
except BrainTimeout:
    ...                       # bump request_timeout, or retry
except BrainError:
    raise
use brain_db_sdk::BrainError;
use brain_db_sdk::wire::types::ErrorCategoryWire;

if let Err(e) = client.encode(&req).await {
    match &e {
        // Authorization covers act_as denied — a wrong-scoped credential; fix it.
        BrainError::Server { category: ErrorCategoryWire::Authorization, .. } => return Err(e.into()),
        // bad input — surface to caller
        BrainError::Server { category: ErrorCategoryWire::Validation, .. } => return Err(e.into()),
        // Protocol / Closed / Timeout / other Server verdicts — see below
        _ => {}
    }
    return Err(e.into());
}

ServerError.category is one of the nine wire categories. Only two are transient — ResourceExhausted and Unavailable (which may carry retryAfterMs). The other seven (Protocol, Authentication, Authorization, Validation, NotFound, Conflict, Internal) will not succeed on a blind retry.

Let the SDK classify retryability

Don't hard-code a status list — use the SDK's own predicate so a category you didn't enumerate still classifies correctly.

import { isRetryable } from "@brain-db/sdk";

try {
  return await client.recall(req);
} catch (e) {
  if (isRetryable(e)) return retryWithBackoff(() => client.recall(req));
  throw e;
}
from brain_db_sdk import is_retryable, with_retry

try:
    return client.recall(req)
except Exception as e:
    if is_retryable(e):
        return with_retry(lambda: client.recall(req))
    raise
use brain_db_sdk::{with_retry, RetryPolicy};

match client.recall(&req).await {
    Ok(answer) => Ok(answer),
    Err(e) if e.is_retryable() => with_retry(&RetryPolicy::default(), || client.recall(&req)).await,
    Err(e) => Err(e),
}

isRetryable / is_retryable returns true for a connection drop, a timeout, or a ResourceExhausted / Unavailable verdict — and false for validation and auth failures. See Retries and cancellation.

Page on Internal / engine.error and persistent Unavailable; log auth and validation failures at WARN — those are your inputs and credentials, not an incident. Never retry a Conflict: a same-id-different-params write will keep conflicting until you fix the request.

Was this page helpful?

On this page