Last Updated: April 2026
This document describes the centralized backend error code registry, error response format, and guidelines for clients and developers.
- Overview
- Error Response Format
- Error Code Registry
- Client Handling Strategies
- Developer Guidelines
- Testing Error Codes
The Commitlabs backend uses a centralized error code registry to ensure:
✅ Consistency — All errors use registered codes (no ad-hoc codes)
✅ Documentation — Each code is documented with meaning and client guidance
✅ Retrievability — Errors include HTTP status, code, message, and optional details
✅ Safety — Registry is validated in tests to prevent regressions
- Source of Truth:
src/lib/backend/errorCodes.tsERROR_CODE_REGISTRY— Complete registry with all error code definitionsgetErrorCodeDefinition()— Retrieve definition by codevalidateErrorCodeRegistry()— Validate registry integrity
- Error Classes:
src/lib/backend/errors.tsApiError— Base class for all errors- Typed subclasses:
BadRequestError,UnauthorizedError,ForbiddenError, etc.
All API errors follow a standardized JSON format:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Email must be a valid email address.",
"details": {
"field": "email",
"constraint": "email_format"
}
}
}| Field | Type | Description |
|---|---|---|
success |
boolean | Always false for errors. |
error.code |
string | Registered error code (from registry). |
error.message |
string | Human-readable error message for display. |
error.details |
object | Optional context-specific details (e.g., validation errors, resource ID). |
All errors include an appropriate HTTP status code:
- 4xx — Client error (request is incorrect or not authorized)
- 5xx — Server error (server failed to process valid request)
This is the complete registry of all error codes. Each code maps to:
- Meaning — What the error represents
- Status Code — HTTP status code
- Client Handling — Recommended client strategy
- Retriable — Whether the request can be safely retried
- Description — When the error is triggered
Meaning: The request was malformed or contains invalid syntax.
Triggered By:
- Malformed JSON body
- Invalid content-type header
- Missing required HTTP headers
- Protocol violations
Client Handling:
// Check request headers, content-type, and method
// Do not retry — the error will persist until request is fixed
console.error("Check your request format and headers");Retriable: ❌ No
Meaning: Request body or query parameters failed validation.
Triggered By:
- Missing required fields
- Invalid field type (e.g., string instead of number)
- Field value out of range
- Format violations (email, date, etc.)
Example:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request data.",
"details": {
"errors": [
{ "field": "commitment_amount", "message": "Must be greater than 0" },
{ "field": "end_date", "message": "Must be after start date" }
]
}
}
}Client Handling:
// Display validation errors in form
response.error.details.errors.forEach((err) => {
displayFieldError(err.field, err.message);
});Retriable: ❌ No
Meaning: Authentication credentials are missing, invalid, or expired.
Triggered By:
- Missing
Authorizationheader - Invalid JWT token (malformed, wrong signature)
- Expired token
- Revoked session
Example:
{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Authentication required.",
"details": { "reason": "token_expired" }
}
}Client Handling:
// 1. Attempt token refresh if refresh_token is available
const newToken = await refreshAuthToken();
if (newToken) {
// Retry request with new token
return retryRequest();
}
// 2. If no refresh token, redirect to login
redirectToLogin();Retriable: ✅ Yes (after token refresh)
Meaning: Authenticated but lacks permission to perform the action.
Triggered By:
- User lacks required role (e.g., admin-only endpoint)
- User is not the resource owner
- Required scope not granted
Example:
{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "You do not have permission to perform this action.",
"details": { "required_role": "admin" }
}
}Client Handling:
// Display permission denied message
toast.error("You lack permission for this action.");
// Log for audit purposes
auditLog.warn("Unauthorized access attempt", {
action: "settle_commitment",
user_id: getCurrentUserId(),
});
// Do not retryRetriable: ❌ No
Meaning: The requested resource does not exist.
Triggered By:
- Resource ID doesn't match any record
- Soft-deleted resource
- Resource moved or removed
Example:
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Commitment not found.",
"details": { "commitment_id": "cm_12345" }
}
}Client Handling:
// Display not found message
displayNotFoundPage();
// Offer navigation alternatives
suggestSearchOrBackButton();
// Do not retryRetriable: ❌ No
Meaning: Request conflicts with the current resource state.
Triggered By:
- Resource already exists (duplicate)
- Commitment already settled
- Resource locked by another user
- Incompatible state transition
Example:
{
"success": false,
"error": {
"code": "CONFLICT",
"message": "Commitment already settled.",
"details": { "settled_at": "2026-04-20T10:30:00Z" }
}
}Client Handling:
// 1. Display conflict message with details
showConflictDialog(response.error.details);
// 2. Fetch latest resource state
const latest = await fetchCommitment(id);
// 3. Prompt user to retry or take alternative action
promptUserToRefreshAndRetry();Retriable: ✅ Yes (after fetching fresh state)
Meaning: Request is syntactically valid but semantically unprocessable.
Triggered By:
- Constraint violations (e.g., insufficient balance)
- Business rule violations (e.g., allocation exceeds limit)
- Invalid amount ranges
Example:
{
"success": false,
"error": {
"code": "UNPROCESSABLE_ENTITY",
"message": "Allocation exceeds maximum allowed percentage.",
"details": { "max_percentage": 100, "attempted": 150 }
}
}Client Handling:
// Show detailed error explaining constraint
showConstraintError(response.error.details);
// Offer user guidance on how to fix
suggestAlternatives(response.error.details);
// Do not retry with same dataRetriable: ❌ No
Meaning: Unexpected server-side failure.
Triggered By:
- Unhandled exception
- Database error
- Logic error in request handler
- Memory exhaustion
Example:
{
"success": false,
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred. Please try again later.",
"details": { "request_id": "req_abc123" }
}
}Client Handling:
// Display generic message
showErrorToast("Something went wrong. Please try again.");
// Implement exponential backoff
await backoffAndRetry({
maxAttempts: 3,
baseDelay: 1000,
maxDelay: 10000,
});
// Log request ID for support
console.error("Request ID:", response.error.details.request_id);Retriable: ✅ Yes (with exponential backoff)
Meaning: Invalid response from upstream server or service.
Triggered By:
- Blockchain RPC error
- Database connection error
- Upstream service returns invalid response
Client Handling:
// Display service unavailable message
showErrorToast("Service temporarily unavailable. Retrying...");
// Implement exponential backoff
await backoffAndRetry();Retriable: ✅ Yes (with exponential backoff)
Meaning: Service is temporarily unavailable (maintenance, overload, or degradation).
Client Handling:
// Respect Retry-After header
const retryAfter = getRetryAfterHeader();
await delay(retryAfter);
return retryRequest();Retriable: ✅ Yes (respect Retry-After header)
Meaning: Upstream service did not respond within the timeout window.
Triggered By:
- Blockchain node slow response
- External API timeout
- Long-running database query
Client Handling:
// Implement exponential backoff with longer timeout
await backoffAndRetry({
timeout: 30000, // Increase from default
maxAttempts: 2,
});Retriable: ✅ Yes
Meaning: Blockchain service is temporarily unavailable.
Triggered By:
- RPC provider unreachable
- Network connectivity issues
- Blockchain node undergoing maintenance
Client Handling:
// Similar to SERVICE_UNAVAILABLE
// Retry with exponential backoffRetriable: ✅ Yes
Meaning: Blockchain operation failed (transaction reverted, execution error, etc.).
Triggered By:
- Smart contract revert
- Invalid transaction parameters
- Insufficient gas or balance
- Logic error in blockchain operation
Example:
{
"success": false,
"error": {
"code": "BLOCKCHAIN_CALL_FAILED",
"message": "Transaction reverted with reason: Insufficient balance.",
"details": {
"reason": "Insufficient balance",
"tx_hash": "0x..."
}
}
}Client Handling:
// Display detailed blockchain error
showBlockchainError(response.error.details);
// Usually not retriable — user must fix underlying issueThe backend centralizes blockchain error normalization in src/lib/backend/errors.ts so contract invocation failures are classified consistently across the service layer.
Retriable: ❌ No
Implement exponential backoff for retriable 5xx errors:
async function retryWithBackoff(fn, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts || !isRetriable(error)) {
throw error;
}
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
const jitter = Math.random() * 1000;
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
}
}
}Retriable Errors (4xx):
UNAUTHORIZED(after token refresh)CONFLICT(after fetching fresh state)
Retriable Errors (5xx):
INTERNAL_ERRORBAD_GATEWAYSERVICE_UNAVAILABLEGATEWAY_TIMEOUTBLOCKCHAIN_UNAVAILABLE
Non-Retriable Errors:
BAD_REQUESTVALIDATION_ERRORFORBIDDENNOT_FOUNDUNPROCESSABLE_ENTITYBLOCKCHAIN_CALL_FAILED
Always use typed error classes from src/lib/backend/errors.ts:
import {
ValidationError,
NotFoundError,
ConflictError,
ForbiddenError,
} from "@/lib/backend/errors";
// ✅ Good: Throw typed error with context
throw new ValidationError("Email must be unique.", {
field: "email",
value: userEmail,
});
throw new NotFoundError("Commitment", { commitmentId });
throw new ConflictError("Commitment already settled.", {
settled_at: new Date().toISOString(),
});
// ❌ Bad: Ad-hoc error codes or messages
throw new Error("Something is wrong"); // No error code!All error codes must be registered in ERROR_CODE_REGISTRY. The registry is validated in tests:
// In test suite
import { validateErrorCodeRegistry } from "@/lib/backend/errorCodes";
describe("Error Code Registry", () => {
it("should contain no duplicates", () => {
const validation = validateErrorCodeRegistry();
expect(validation.valid).toBe(true);
expect(validation.duplicates).toHaveLength(0);
expect(validation.errors).toHaveLength(0);
});
});To add a new error code:
-
Add to registry in
src/lib/backend/errorCodes.ts:export const ERROR_CODE_REGISTRY: Record<string, ErrorCodeDefinition> = { // ... existing codes MY_NEW_ERROR: { code: "MY_NEW_ERROR", statusCode: 400, meaning: "Clear description...", clientHandling: "How clients should handle this...", retriable: false, description: "When this error is triggered...", }, };
-
Create typed error class if needed in
src/lib/backend/errors.ts:export class MyNewError extends ApiError { constructor(message = "Default message.", details?: unknown) { super(message, "MY_NEW_ERROR", 400, details); this.name = "MyNewError"; } }
-
Update documentation in this file with explanation and examples.
-
Add tests to ensure the code is registered and validated.
// tests/lib/backend/errorCodes.test.ts
import {
ERROR_CODE_REGISTRY,
validateErrorCodeRegistry,
getErrorCodesByStatus,
} from "@/lib/backend/errorCodes";
describe("ERROR_CODE_REGISTRY", () => {
it("should contain all required error codes", () => {
const requiredCodes = [
"BAD_REQUEST",
"VALIDATION_ERROR",
"UNAUTHORIZED",
"FORBIDDEN",
"NOT_FOUND",
"CONFLICT",
"UNPROCESSABLE_ENTITY",
"TOO_MANY_REQUESTS",
"INTERNAL_ERROR",
"BAD_GATEWAY",
"SERVICE_UNAVAILABLE",
"GATEWAY_TIMEOUT",
];
requiredCodes.forEach((code) => {
expect(ERROR_CODE_REGISTRY[code]).toBeDefined();
});
});
it("should have no duplicate error codes", () => {
const validation = validateErrorCodeRegistry();
expect(validation.valid).toBe(true);
expect(validation.duplicates).toHaveLength(0);
});
it("should have all required fields in each definition", () => {
const validation = validateErrorCodeRegistry();
expect(validation.errors).toHaveLength(0);
});
it("should group codes by status correctly", () => {
const grouped = getErrorCodesByStatus();
expect(grouped[400]).toBeDefined(); // Client errors
expect(grouped[500]).toBeDefined(); // Server errors
});
it("should retrieve definition by code", () => {
const { getErrorCodeDefinition } = require("@/lib/backend/errorCodes");
const def = getErrorCodeDefinition("VALIDATION_ERROR");
expect(def.code).toBe("VALIDATION_ERROR");
expect(def.statusCode).toBe(400);
expect(def.retriable).toBe(false);
});
});// tests/api/sample.test.ts
describe("API Error Responses", () => {
it("should return VALIDATION_ERROR for invalid input", async () => {
const response = await fetch("/api/commitments", {
method: "POST",
body: JSON.stringify({ amount: "invalid" }),
});
expect(response.status).toBe(400);
const data = await response.json();
expect(data.error.code).toBe("VALIDATION_ERROR");
expect(data.success).toBe(false);
});
});- Error Code Registry:
src/lib/backend/errorCodes.ts - Error Classes:
src/lib/backend/errors.ts - API Response Format:
src/lib/backend/apiResponse.ts - Error Wrapper:
src/lib/backend/withApiHandler.ts