Skip to content

Errors

API error envelope, error codes, validation error shape, and SDK error classes.

Updated 2026-05-20

Every error response uses the same envelope. The code field is stable across versions and safe to match against; the message field is human-friendly and may change.

Envelope

{
  "error": {
    "code": "validation_failed",
    "message": "Required field 'title' was missing.",
    "details": { "fieldErrors": { "title": ["String must contain at least 1 character"] } },
    "traceId": "trc_8X3FpQk"
  }
}
  • code: machine-readable, stable, snake_case
  • message: human-readable, may evolve
  • details: optional structured context (field-level errors, conflicting slug, etc.)
  • traceId: same value as the x-faq-trace-id response header; quote this to support

Common error codes

CodeHTTPWhen
unauthorized401Missing / malformed Authorization header
invalid_api_key401API key doesn’t match any active key
insufficient_scope403Key’s scopes don’t include the required one
not_found404Resource doesn’t exist or belongs to a different org
slug_conflict409A resource with this slug already exists
validation_failed400Request body / params failed Zod validation
invalid_json400Body wasn’t valid JSON
rate_limit_exceeded429Quota or burst limit hit; see Retry-After header
plan_limit_reached402Org hit a plan limit (questions, keys, requests, etc.)
internal_error500Server-side bug; the traceId is the key to finding it

Resource-specific codes (e.g. question_not_found, category_not_empty) are documented per endpoint.

Validation errors

Zod-validated endpoints return field-level details:

{
  "error": {
    "code": "validation_failed",
    "message": "1 invalid field",
    "details": {
      "fieldErrors": { "answer": ["String must contain at least 1 character"] },
      "formErrors": []
    }
  }
}

fieldErrors keys map directly to your request body’s shape. formErrors carries issues that apply to the request as a whole (e.g. mutually exclusive fields).

SDK error classes

@faqapp/core throws typed error classes so you can handle each case explicitly:

import {
  FAQAPIError,
  FAQValidationError,
  FAQNotFoundError,
  FAQRateLimitError,
  FAQNetworkError,
  FAQTimeoutError
} from "@faqapp/core";

try {
  await faq.questions.create({ title: "", answer: "" });
} catch (err) {
  if (err instanceof FAQValidationError) {
    console.error(err.errors); // ValidationIssue[]
  } else if (err instanceof FAQNotFoundError) {
    // 404, resource doesn't exist
  } else if (err instanceof FAQRateLimitError) {
    await sleep(err.retryAfter * 1000);
  } else if (err instanceof FAQAPIError) {
    // any other 4xx/5xx; err.code, err.status, err.traceId available
  } else if (err instanceof FAQNetworkError) {
    // DNS / connection refused / no route
  } else if (err instanceof FAQTimeoutError) {
    // exceeded the SDK's configured timeout
  }
}

Best practices

  • Match on code, not message. Codes are stable; messages are not.
  • Log the traceId. Quote it in any support email; we look up the exact request in seconds.
  • Backoff on 429. Use the Retry-After value; exponential backoff for repeated failures.
  • Don’t show raw message to end users. Map codes to UI copy you control.
  • Treat network errors as different from API errors. A timeout is not a 500; retry strategy differs.