Skip to main content

Overview

The High IQ API uses structured error responses with machine-readable codes and human-readable messages. Every error follows the same envelope format, making client-side error handling consistent and predictable.

Error Response Format

All errors return success: false with a structured error object:
{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Strain not found: purple-unicorn",
    "details": {
      "resource": "Strain",
      "id": "purple-unicorn"
    }
  },
  "meta": {
    "timestamp": "2026-02-16T12:00:00.000Z",
    "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "path": "/api/v1/strains/slug/purple-unicorn",
    "method": "GET"
  }
}

Error Fields

FieldTypeDescription
error.codestringMachine-readable error code (e.g., NOT_FOUND, BAD_REQUEST)
error.messagestringHuman-readable description of the error
error.detailsunknownOptional additional context (varies by error type)
error.stackstringStack trace (non-production environments only)
meta.timestampstringISO 8601 timestamp of when the error occurred
meta.requestIdstringUnique request identifier for debugging
meta.pathstringThe request path that triggered the error
meta.methodstringThe HTTP method used
The error.stack field is only included in non-production environments. In production, stack traces are stripped to avoid leaking internal implementation details.

Error Codes

Client Errors (4xx)

CodeHTTP StatusDescription
BAD_REQUEST400Invalid request parameters or body
VALIDATION_ERROR400Request body failed schema validation
UNAUTHORIZED401Authentication required but not provided
INVALID_TOKEN401JWT signature verification failed
TOKEN_EXPIRED401JWT has expired
FORBIDDEN403Valid auth but insufficient permissions
INSUFFICIENT_PERMISSIONS403User lacks required role or permission
NOT_FOUND404Requested resource does not exist
RATE_LIMIT_EXCEEDED429Too many requests in the time window

Resource-Specific Errors (4xx)

CodeHTTP StatusDescription
STRAIN_NOT_FOUND404Strain with the given slug or ID does not exist
ORDER_NOT_FOUND404Order with the given ID does not exist
REPORT_NOT_FOUND404Report with the given ID does not exist
USER_NOT_FOUND404User with the given ID does not exist

Server Errors (5xx)

CodeHTTP StatusDescription
INTERNAL_ERROR500Unexpected server error
DATABASE_ERROR500Database operation failed
EXTERNAL_SERVICE_ERROR500Third-party service (AI, external API) failed

Common Error Examples

Not Found

curl "https://tiwih-api.vercel.app/api/v1/strains/slug/nonexistent-strain"
{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Strain not found: nonexistent-strain",
    "details": {
      "resource": "Strain",
      "id": "nonexistent-strain"
    }
  }
}

Validation Error

When a request body fails Zod schema validation, the API returns a VALIDATION_ERROR with the full list of validation issues in the details field.
curl -X POST "https://tiwih-api.vercel.app/api/v1/reports/stream/batch" \
  -H "Content-Type: application/json" \
  -d '{"sectionKeys": []}'
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [
      {
        "code": "too_small",
        "minimum": 1,
        "type": "array",
        "inclusive": true,
        "exact": false,
        "message": "At least one section key is required",
        "path": ["sectionKeys"]
      }
    ]
  }
}

Bad Request

{
  "success": false,
  "error": {
    "code": "BAD_REQUEST",
    "message": "Invalid section key",
    "details": {
      "invalidKeys": ["not_a_real_section"],
      "validKeys": ["overview", "terpene_analysis", "effects_breakdown"]
    }
  }
}

Rate Limit Exceeded

{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded",
    "details": {
      "retryAfter": 45
    }
  }
}

Database Error

In non-production environments, database errors include the underlying error for debugging. In production, they return a generic message.
{
  "success": false,
  "error": {
    "code": "DATABASE_ERROR",
    "message": "Database operation failed"
  }
}

CommonErrors Helper

The API uses a CommonErrors helper object to generate consistent error responses throughout the codebase. These are the pre-built error factories:
HelperHTTP StatusCodeDefault Message
CommonErrors.notFound(c, resource, id?)404NOT_FOUND"{resource} not found: {id}"
CommonErrors.unauthorized(c, message?)401UNAUTHORIZED"Authentication required"
CommonErrors.forbidden(c, message?)403FORBIDDEN"Insufficient permissions"
CommonErrors.badRequest(c, message, details?)400BAD_REQUESTCustom message
CommonErrors.validationError(c, errors)400VALIDATION_ERROR"Validation failed"
CommonErrors.databaseError(c, error)500DATABASE_ERROR"Database operation failed"
CommonErrors.internalError(c, error?)500INTERNAL_ERROR"An unexpected error occurred"
CommonErrors.rateLimitExceeded(c, retryAfter?)429RATE_LIMIT_EXCEEDED"Rate limit exceeded"

Custom ApiError Class

For route handlers that need to throw errors, the API provides a custom ApiError class:
import { ApiError } from '../lib/api-response.js';

// Throw a structured error
throw new ApiError(
  404,                    // HTTP status code
  'STRAIN_NOT_FOUND',    // Error code
  'Strain not found',    // Human-readable message
  { slug: 'bad-strain' } // Optional details
);
The global error handler catches ApiError instances and formats them into the standard error response envelope.

Client-Side Handling

TypeScript Type Guard

Use the provided type guards to safely check response types:
import type { ApiResponse, ApiSuccessResponse, ApiErrorResponse } from '@tiwih/types';

function isErrorResponse(response: ApiResponse): response is ApiErrorResponse {
  return !response.success;
}

function isSuccessResponse<T>(response: ApiResponse<T>): response is ApiSuccessResponse<T> {
  return response.success;
}

// Usage
const response = await fetch('/api/v1/strains/slug/blue-dream');
const data: ApiResponse<Strain> = await response.json();

if (isErrorResponse(data)) {
  console.error(`Error ${data.error.code}: ${data.error.message}`);
} else {
  console.log(data.data); // Typed as Strain
}

HTTP Status Code Handling

Always check the HTTP status code first, then parse the response body for details:
const response = await fetch(url);

switch (response.status) {
  case 200:
    return (await response.json()).data;
  case 400:
    throw new Error('Invalid request');
  case 401:
    // Redirect to login or refresh token
    break;
  case 404:
    return null;
  case 429:
    // Respect Retry-After header
    const retryAfter = response.headers.get('Retry-After');
    break;
  case 500:
    throw new Error('Server error');
}