API docs / Error handling

HTTP Client

Error handling

Handle errors, configure retries, and understand error codes.

The SDK uses a tuple-based error pattern inspired by Go's error handling. Every request() call returns [error, null] on failure or [null, data] on success: no try/catch needed for normal error flow.

The error tuple

const [error, data] = await account.request("GET /users/me", {})

if (error) {
  // error is OfApiError
  console.error(error.code)    // "RATE_LIMITED" | "UNAUTHORIZED" | ...
  console.error(error.status)  // HTTP status code
  console.error(error.message) // Human-readable description
  return
}

// data is fully typed — no null check needed
console.log(data.name)

This pattern makes it impossible to accidentally use the response without checking for errors first.

Error codes

The code field on OfApiError is one of these values:

CodeStatusMeaning
UNAUTHORIZED401Invalid or missing API key
FORBIDDEN403Valid key but insufficient access
NOT_FOUND404Route or resource doesn't exist
RATE_LIMITED429Too many requests, retried automatically
API_ERRORotherUpstream error (including 5xx, retried automatically)
NETWORKn/aConnection failed, retried automatically
PARSE_ERRORn/aResponse body could not be parsed

Automatic retries

GET requests are retried automatically on 429, 5xx, and network errors. The SDK backs off between attempts (scaling with the attempt number) with added jitter.

Default retry configuration:

OptionTypeDefaultDescription
maxAttemptsnumber3Maximum number of attempts (including the first).
backoffMsnumber1000Base delay in milliseconds before retrying. Scales with the attempt number.

Override retries per-client or per-request:

Per client
const client = new OfApiClient({
  apiKey: process.env.OFM_API_KEY,
  retry: { maxAttempts: 5, backoffMs: 2000 },
})
Per request
const [error, data] = await account.request(
  "GET /chats",
  { query: { limit: 20 } },
  { retry: 1 },
)

Only GET requests are retried by default. POST, PUT, and DELETE requests are not retried to avoid duplicate side effects.

Using fetch() with try/catch

If you prefer the throw pattern, use fetch() instead of request():

import { OfApiError } from "@betterfans/link-sdk"

try {
  const me = await account.fetch("GET /users/me", {})
  console.log(me.name)
} catch (error) {
  if (error instanceof OfApiError) {
    switch (error.code) {
      case "RATE_LIMITED":
        console.log("Rate limited, try again later")
        break
      case "UNAUTHORIZED":
        console.log("Check your API key")
        break
      default:
        console.error("Request failed:", error.message)
    }
  }
}

Checking error types

OfApiError is exported from the SDK for instanceof checks:

import { OfApiError } from "@betterfans/link-sdk"

const [error, data] = await account.request("GET /users/me", {})

if (error) {
  if (error.status === 429) {
    // Rate limited — back off
  }
  if (error.code === "NOT_FOUND") {
    // Resource doesn't exist
  }
}