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.
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
| Field | Type | Description |
|---|
error.code | string | Machine-readable error code (e.g., NOT_FOUND, BAD_REQUEST) |
error.message | string | Human-readable description of the error |
error.details | unknown | Optional additional context (varies by error type) |
error.stack | string | Stack trace (non-production environments only) |
meta.timestamp | string | ISO 8601 timestamp of when the error occurred |
meta.requestId | string | Unique request identifier for debugging |
meta.path | string | The request path that triggered the error |
meta.method | string | The 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)
| Code | HTTP Status | Description |
|---|
BAD_REQUEST | 400 | Invalid request parameters or body |
VALIDATION_ERROR | 400 | Request body failed schema validation |
UNAUTHORIZED | 401 | Authentication required but not provided |
INVALID_TOKEN | 401 | JWT signature verification failed |
TOKEN_EXPIRED | 401 | JWT has expired |
FORBIDDEN | 403 | Valid auth but insufficient permissions |
INSUFFICIENT_PERMISSIONS | 403 | User lacks required role or permission |
NOT_FOUND | 404 | Requested resource does not exist |
RATE_LIMIT_EXCEEDED | 429 | Too many requests in the time window |
Resource-Specific Errors (4xx)
| Code | HTTP Status | Description |
|---|
STRAIN_NOT_FOUND | 404 | Strain with the given slug or ID does not exist |
ORDER_NOT_FOUND | 404 | Order with the given ID does not exist |
REPORT_NOT_FOUND | 404 | Report with the given ID does not exist |
USER_NOT_FOUND | 404 | User with the given ID does not exist |
Server Errors (5xx)
| Code | HTTP Status | Description |
|---|
INTERNAL_ERROR | 500 | Unexpected server error |
DATABASE_ERROR | 500 | Database operation failed |
EXTERNAL_SERVICE_ERROR | 500 | Third-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"
}
}
{
"success": false,
"error": {
"code": "DATABASE_ERROR",
"message": "Database operation failed",
"details": {
"message": "relation \"strains_v2\" does not exist",
"code": "42P01"
}
}
}
CommonErrors Helper
The API uses a CommonErrors helper object to generate consistent error responses throughout the codebase. These are the pre-built error factories:
| Helper | HTTP Status | Code | Default Message |
|---|
CommonErrors.notFound(c, resource, id?) | 404 | NOT_FOUND | "{resource} not found: {id}" |
CommonErrors.unauthorized(c, message?) | 401 | UNAUTHORIZED | "Authentication required" |
CommonErrors.forbidden(c, message?) | 403 | FORBIDDEN | "Insufficient permissions" |
CommonErrors.badRequest(c, message, details?) | 400 | BAD_REQUEST | Custom message |
CommonErrors.validationError(c, errors) | 400 | VALIDATION_ERROR | "Validation failed" |
CommonErrors.databaseError(c, error) | 500 | DATABASE_ERROR | "Database operation failed" |
CommonErrors.internalError(c, error?) | 500 | INTERNAL_ERROR | "An unexpected error occurred" |
CommonErrors.rateLimitExceeded(c, retryAfter?) | 429 | RATE_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');
}