Skip to main content

Error Handling

NeuroLink server adapters provide a comprehensive error handling system with typed error classes, automatic recovery strategies, and structured error responses. This guide covers the complete error hierarchy and how to handle errors effectively.


Error Architecture Overview

The server adapter error system is built around:

  1. Typed Error Classes - 23 specialized error classes extending ServerAdapterError
  2. Error Categories - 9 categories for logical grouping
  3. Severity Levels - 4 levels for prioritization
  4. Recovery Strategies - Automatic retry and backoff configurations
  5. HTTP Status Mapping - Consistent HTTP status code mapping
ServerAdapterError (base class)
├── ConfigurationError
├── RouteConflictError
├── RouteNotFoundError
├── ValidationError
├── AuthenticationError
├── InvalidAuthenticationError
├── AuthorizationError
├── RateLimitError
├── TimeoutError
├── HandlerError
├── StreamingError
├── StreamAbortedError
├── WebSocketError
├── WebSocketConnectionError
├── ServerStartError
├── ServerStopError
├── AlreadyRunningError
├── NotRunningError
├── ShutdownTimeoutError
├── DrainTimeoutError
├── InvalidLifecycleStateError
└── MissingDependencyError

Error Categories

Errors are grouped into 9 categories that determine handling behavior and recovery strategies:

CategoryDescriptionRecovery Strategy
CONFIGConfiguration and setup errorsFail immediately
VALIDATIONInput validation and schema errorsFail immediately
EXECUTIONRuntime handler and processing errorsRetry (3 attempts)
EXTERNALExternal service and dependency errorsExponential backoff
RATE_LIMITRate limiting exceededExponential backoff
AUTHENTICATIONMissing or invalid authenticationFail immediately
AUTHORIZATIONPermission and access denied errorsFail immediately
STREAMINGStreaming and SSE errorsRetry (2 attempts)
WEBSOCKETWebSocket connection and message errorsExponential backoff

Severity Levels

Each error has a severity level for logging and alerting:

SeverityDescriptionExample Errors
LOWMinor issues, typically user errorsRouteNotFoundError, StreamAbortedError
MEDIUMModerate issues that may need attentionTimeoutError, AuthenticationError
HIGHSerious issues that should be investigatedHandlerError, ConfigurationError
CRITICALSystem-level failures requiring immediate actionServerStartError, MissingDependencyError

Error Classes Reference

Base Class: ServerAdapterError

All server adapter errors extend this base class:

import { ServerAdapterError } from "@juspay/neurolink";

class ServerAdapterError extends Error {
readonly code: string; // Unique error code
readonly category: string; // Error category
readonly severity: string; // Severity level
readonly retryable: boolean; // Whether retry is recommended
readonly retryAfterMs?: number; // Suggested retry delay
readonly requestId?: string; // Request identifier for tracing
readonly path?: string; // Request path
readonly method?: string; // HTTP method
readonly details?: object; // Additional error details
readonly cause?: Error; // Original error if wrapped

toJSON(): object; // Serialize for API response
getHttpStatus(): number; // Get appropriate HTTP status
}

Configuration Errors

ConfigurationError

Thrown when server configuration is invalid.

import { ConfigurationError } from "@juspay/neurolink";

throw new ConfigurationError(
"Invalid port number: must be between 1 and 65535",
{ port: 99999, field: "port" },
);
PropertyValue
CodeSERVER_ADAPTER_INVALID_CONFIG
CategoryCONFIG
SeverityHIGH
HTTP Status400
RetryableNo

MissingDependencyError

Thrown when a required framework dependency is not installed.

import { MissingDependencyError } from "@juspay/neurolink";

throw new MissingDependencyError("express", "Express", "npm install express");
PropertyValue
CodeSERVER_ADAPTER_MISSING_DEPENDENCY
CategoryCONFIG
SeverityCRITICAL
HTTP Status500
RetryableNo

Route Errors

RouteConflictError

Thrown when registering a route that conflicts with an existing route.

import { RouteConflictError } from "@juspay/neurolink";

throw new RouteConflictError("/api/users/:id", "GET", "/api/users/:userId");
PropertyValue
CodeSERVER_ADAPTER_ROUTE_CONFLICT
CategoryCONFIG
SeverityHIGH
HTTP Status500
RetryableNo

RouteNotFoundError

Thrown when a requested route does not exist.

import { RouteNotFoundError } from "@juspay/neurolink";

throw new RouteNotFoundError("/api/unknown", "GET", "req-123");
PropertyValue
CodeSERVER_ADAPTER_ROUTE_NOT_FOUND
CategoryVALIDATION
SeverityLOW
HTTP Status404
RetryableNo

Validation Errors

ValidationError

Thrown when request validation fails.

import { ValidationError } from "@juspay/neurolink";

throw new ValidationError(
[
{ field: "email", message: "Invalid email format", value: "not-an-email" },
{ field: "age", message: "Must be a positive number", value: -5 },
],
"req-123",
);
PropertyValue
CodeSERVER_ADAPTER_VALIDATION_ERROR
CategoryVALIDATION
SeverityLOW
HTTP Status400
RetryableNo

Authentication & Authorization Errors

AuthenticationError

Thrown when authentication is required but not provided.

import { AuthenticationError } from "@juspay/neurolink";

throw new AuthenticationError("Bearer token required", "req-123");
PropertyValue
CodeSERVER_ADAPTER_AUTH_REQUIRED
CategoryAUTHENTICATION
SeverityMEDIUM
HTTP Status401
RetryableNo

InvalidAuthenticationError

Thrown when provided authentication credentials are invalid.

import { InvalidAuthenticationError } from "@juspay/neurolink";

throw new InvalidAuthenticationError("Token expired", "req-123");
PropertyValue
CodeSERVER_ADAPTER_AUTH_INVALID
CategoryAUTHENTICATION
SeverityMEDIUM
HTTP Status401
RetryableNo

AuthorizationError

Thrown when the authenticated user lacks required permissions.

import { AuthorizationError } from "@juspay/neurolink";

throw new AuthorizationError(
"Insufficient permissions to access this resource",
"req-123",
["admin", "moderator"],
);
PropertyValue
CodeSERVER_ADAPTER_FORBIDDEN
CategoryAUTHORIZATION
SeverityMEDIUM
HTTP Status403
RetryableNo

Rate Limiting Errors

RateLimitError

Thrown when request rate limits are exceeded.

import { RateLimitError } from "@juspay/neurolink";

throw new RateLimitError(
60000, // retry after 60 seconds
"Rate limit exceeded: 100 requests per minute",
"req-123",
);
PropertyValue
CodeSERVER_ADAPTER_RATE_LIMIT_EXCEEDED
CategoryRATE_LIMIT
SeverityMEDIUM
HTTP Status429
RetryableYes

Execution Errors

TimeoutError

Thrown when an operation exceeds its timeout.

import { TimeoutError } from "@juspay/neurolink";

throw new TimeoutError(30000, "AI generation", "req-123");
PropertyValue
CodeSERVER_ADAPTER_TIMEOUT
CategoryEXECUTION
SeverityMEDIUM
HTTP Status408
RetryableYes

HandlerError

Thrown when a route handler fails during execution.

import { HandlerError } from "@juspay/neurolink";

throw new HandlerError(
"Failed to process request",
originalError,
"req-123",
"/api/agent/execute",
"POST",
);
PropertyValue
CodeSERVER_ADAPTER_HANDLER_ERROR
CategoryEXECUTION
SeverityHIGH
HTTP Status500
RetryableNo

Streaming Errors

StreamingError

Thrown when a streaming operation fails.

import { StreamingError } from "@juspay/neurolink";

throw new StreamingError("Stream write failed", originalError, "req-123");
PropertyValue
CodeSERVER_ADAPTER_STREAM_ERROR
CategorySTREAMING
SeverityMEDIUM
HTTP Status500
RetryableNo

StreamAbortedError

Thrown when a client aborts a streaming connection.

import { StreamAbortedError } from "@juspay/neurolink";

throw new StreamAbortedError("Client disconnected", "req-123");
PropertyValue
CodeSERVER_ADAPTER_STREAM_ABORTED
CategorySTREAMING
SeverityLOW
HTTP Status499
RetryableNo

WebSocket Errors

WebSocketError

General WebSocket operation errors.

import { WebSocketError } from "@juspay/neurolink";

throw new WebSocketError("Message send failed", originalError, "ws-conn-123");
PropertyValue
CodeSERVER_ADAPTER_WEBSOCKET_ERROR
CategoryWEBSOCKET
SeverityMEDIUM
HTTP Status500
RetryableYes

WebSocketConnectionError

Thrown when WebSocket connection establishment fails.

import { WebSocketConnectionError } from "@juspay/neurolink";

throw new WebSocketConnectionError("Handshake failed", originalError);
PropertyValue
CodeSERVER_ADAPTER_WEBSOCKET_CONNECTION_FAILED
CategoryWEBSOCKET
SeverityHIGH
HTTP Status500
RetryableYes

Server Lifecycle Errors

ServerStartError

Thrown when the server fails to start.

import { ServerStartError } from "@juspay/neurolink";

throw new ServerStartError(
"Port already in use",
originalError,
3000,
"0.0.0.0",
);
PropertyValue
CodeSERVER_ADAPTER_START_FAILED
CategoryCONFIG
SeverityCRITICAL
HTTP Status500
RetryableYes

ServerStopError

Thrown when the server fails to stop cleanly.

import { ServerStopError } from "@juspay/neurolink";

throw new ServerStopError("Failed to close connections", originalError);
PropertyValue
CodeSERVER_ADAPTER_STOP_FAILED
CategoryEXECUTION
SeverityHIGH
HTTP Status500
RetryableNo

AlreadyRunningError

Thrown when attempting to start an already running server.

import { AlreadyRunningError } from "@juspay/neurolink";

throw new AlreadyRunningError(3000, "0.0.0.0");
PropertyValue
CodeSERVER_ADAPTER_ALREADY_RUNNING
CategoryCONFIG
SeverityLOW
HTTP Status500
RetryableNo

NotRunningError

Thrown when attempting to stop a server that is not running.

import { NotRunningError } from "@juspay/neurolink";

throw new NotRunningError();
PropertyValue
CodeSERVER_ADAPTER_NOT_RUNNING
CategoryCONFIG
SeverityLOW
HTTP Status500
RetryableNo

ShutdownTimeoutError

Thrown when graceful shutdown exceeds the configured timeout.

import { ShutdownTimeoutError } from "@juspay/neurolink";

throw new ShutdownTimeoutError(30000, 5); // 30s timeout, 5 remaining connections
PropertyValue
CodeSERVER_ADAPTER_STOP_FAILED
CategoryEXECUTION
SeverityHIGH
HTTP Status500
RetryableNo

DrainTimeoutError

Thrown when connection draining exceeds the configured timeout.

import { DrainTimeoutError } from "@juspay/neurolink";

throw new DrainTimeoutError(10000, 3); // 10s timeout, 3 remaining connections
PropertyValue
CodeSERVER_ADAPTER_STOP_FAILED
CategoryEXECUTION
SeverityMEDIUM
HTTP Status500
RetryableNo

InvalidLifecycleStateError

Thrown when an operation is attempted in an invalid server state.

import { InvalidLifecycleStateError } from "@juspay/neurolink";

throw new InvalidLifecycleStateError("start", "stopping", [
"stopped",
"initialized",
]);
PropertyValue
CodeSERVER_ADAPTER_INVALID_LIFECYCLE_STATE
CategoryCONFIG
SeverityMEDIUM
HTTP Status500
RetryableNo

HTTP Status Code Mapping

Errors automatically map to appropriate HTTP status codes:

Error CodeHTTP StatusDescription
VALIDATION_ERROR400Bad Request
SCHEMA_ERROR400Bad Request
INVALID_CONFIG400Bad Request
INVALID_ROUTE400Bad Request
AUTH_REQUIRED401Unauthorized
AUTH_INVALID401Unauthorized
FORBIDDEN403Forbidden
ROUTE_NOT_FOUND404Not Found
TIMEOUT408Request Timeout
RATE_LIMIT_EXCEEDED429Too Many Requests
STREAM_ABORTED499Client Closed Request
All other errors500Internal Server Error

Error Response Format

All errors are serialized to a consistent JSON format:

{
"error": {
"code": "SERVER_ADAPTER_VALIDATION_ERROR",
"message": "Validation failed: Invalid email format, Must be a positive number",
"category": "VALIDATION",
"requestId": "req-abc123",
"details": {
"errors": [
{
"field": "email",
"message": "Invalid email format",
"value": "not-an-email"
},
{ "field": "age", "message": "Must be a positive number", "value": -5 }
]
},
"retryAfter": 60
}
}

Response Fields

FieldTypeDescription
codestringUnique error code for programmatic handling
messagestringHuman-readable error message
categorystringError category for grouping
requestIdstringRequest ID for tracing (when available)
detailsobjectAdditional context-specific information
retryAfternumberSuggested retry delay in seconds (for retryable errors)

Recovery Strategies

Each error category has a predefined recovery strategy:

const ErrorRecoveryStrategies = {
CONFIG: {
strategy: "fail",
maxRetries: 0,
baseDelayMs: 0,
},
VALIDATION: {
strategy: "fail",
maxRetries: 0,
baseDelayMs: 0,
},
EXECUTION: {
strategy: "retry",
maxRetries: 3,
baseDelayMs: 1000,
},
EXTERNAL: {
strategy: "exponentialBackoff",
maxRetries: 5,
baseDelayMs: 1000,
},
RATE_LIMIT: {
strategy: "exponentialBackoff",
maxRetries: 3,
baseDelayMs: 5000,
},
AUTHENTICATION: {
strategy: "fail",
maxRetries: 0,
baseDelayMs: 0,
},
AUTHORIZATION: {
strategy: "fail",
maxRetries: 0,
baseDelayMs: 0,
},
STREAMING: {
strategy: "retry",
maxRetries: 2,
baseDelayMs: 500,
},
WEBSOCKET: {
strategy: "exponentialBackoff",
maxRetries: 5,
baseDelayMs: 1000,
},
};

Strategy Types

StrategyDescription
failFail immediately without retry
retryRetry with fixed delay between attempts
exponentialBackoffRetry with exponentially increasing delays (1s, 2s, 4s, 8s, ...)

Custom Error Handling

Global Error Handler

Register a global error handler for custom error processing:

import { createServer, ServerAdapterError, wrapError } from "@juspay/neurolink";

const server = await createServer(neurolink, {
framework: "hono",
config: { port: 3000 },
});

// Register global error handler
server.onError((error, context) => {
// Wrap unknown errors
const serverError =
error instanceof ServerAdapterError
? error
: wrapError(error, context.requestId, context.path, context.method);

// Log based on severity
if (serverError.severity === "CRITICAL") {
alertOps(serverError);
}

if (serverError.severity === "HIGH" || serverError.severity === "CRITICAL") {
logger.error("Server error", {
code: serverError.code,
message: serverError.message,
requestId: serverError.requestId,
path: serverError.path,
stack: serverError.stack,
});
}

// Track metrics
metrics.increment("server.errors", {
code: serverError.code,
category: serverError.category,
severity: serverError.severity,
});

// Return the error (will be serialized to JSON response)
return serverError;
});

Route-Level Error Handling

Handle errors in specific routes:

server.registerRoute({
method: "POST",
path: "/api/custom",
handler: async (ctx) => {
try {
const result = await processRequest(ctx.body);
return result;
} catch (error) {
// Transform domain errors to server errors
if (error instanceof DomainValidationError) {
throw new ValidationError(
[{ field: error.field, message: error.message }],
ctx.requestId,
);
}

if (error instanceof ExternalServiceError) {
throw new HandlerError(
"External service unavailable",
error,
ctx.requestId,
ctx.path,
ctx.method,
);
}

// Re-throw server adapter errors
throw error;
}
},
});

Using wrapError Helper

The wrapError utility converts unknown errors to ServerAdapterError:

import { wrapError, ServerAdapterError } from "@juspay/neurolink";

function handleError(error: unknown, requestId: string): ServerAdapterError {
// Already a ServerAdapterError - return as-is
if (error instanceof ServerAdapterError) {
return error;
}

// Wrap as HandlerError
return wrapError(error, requestId, "/api/endpoint", "POST");
}

Implementing Retry Logic

Use recovery strategies for automatic retry:

import { ErrorRecoveryStrategies, ServerAdapterError } from "@juspay/neurolink";

async function executeWithRetry<T>(
operation: () => Promise<T>,
category: string,
): Promise<T> {
const strategy = ErrorRecoveryStrategies[category];
let lastError: Error | undefined;

for (let attempt = 0; attempt <= strategy.maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;

// Don't retry if strategy is "fail"
if (strategy.strategy === "fail") {
throw error;
}

// Check if error is retryable
if (error instanceof ServerAdapterError && !error.retryable) {
throw error;
}

// Calculate delay
const delay =
strategy.strategy === "exponentialBackoff"
? strategy.baseDelayMs * Math.pow(2, attempt)
: strategy.baseDelayMs;

// Use retryAfterMs if provided
const actualDelay =
error instanceof ServerAdapterError && error.retryAfterMs
? error.retryAfterMs
: delay;

if (attempt < strategy.maxRetries) {
await sleep(actualDelay);
}
}
}

throw lastError;
}

Error Codes Reference

Configuration Errors

CodeDescription
SERVER_ADAPTER_INVALID_CONFIGInvalid server configuration
SERVER_ADAPTER_MISSING_DEPENDENCYRequired framework dependency not found
SERVER_ADAPTER_FRAMEWORK_INIT_FAILEDFramework initialization failed

Route Errors

CodeDescription
SERVER_ADAPTER_ROUTE_NOT_FOUNDRequested route does not exist
SERVER_ADAPTER_ROUTE_CONFLICTRoute conflicts with existing route
SERVER_ADAPTER_INVALID_ROUTEInvalid route definition

Execution Errors

CodeDescription
SERVER_ADAPTER_HANDLER_ERRORRoute handler execution failed
SERVER_ADAPTER_TIMEOUTOperation timed out
SERVER_ADAPTER_MIDDLEWARE_ERRORMiddleware execution failed

Authentication/Authorization Errors

CodeDescription
SERVER_ADAPTER_AUTH_REQUIREDAuthentication required but not provided
SERVER_ADAPTER_AUTH_INVALIDInvalid authentication credentials
SERVER_ADAPTER_FORBIDDENAccess denied (insufficient permissions)

Rate Limiting Errors

CodeDescription
SERVER_ADAPTER_RATE_LIMIT_EXCEEDEDRequest rate limit exceeded

Streaming Errors

CodeDescription
SERVER_ADAPTER_STREAM_ERRORStreaming operation failed
SERVER_ADAPTER_STREAM_ABORTEDClient aborted the stream

WebSocket Errors

CodeDescription
SERVER_ADAPTER_WEBSOCKET_ERRORWebSocket operation failed
SERVER_ADAPTER_WEBSOCKET_CONNECTION_FAILEDWebSocket connection failed

Validation Errors

CodeDescription
SERVER_ADAPTER_VALIDATION_ERRORRequest validation failed
SERVER_ADAPTER_SCHEMA_ERRORSchema validation failed

Lifecycle Errors

CodeDescription
SERVER_ADAPTER_START_FAILEDServer failed to start
SERVER_ADAPTER_STOP_FAILEDServer failed to stop
SERVER_ADAPTER_ALREADY_RUNNINGServer is already running
SERVER_ADAPTER_NOT_RUNNINGServer is not running

Best Practices

1. Use Specific Error Classes

Throw the most specific error class for your situation:

// Good - specific error with context
throw new ValidationError(
[{ field: "email", message: "Invalid format" }],
requestId,
);

// Avoid - generic error
throw new Error("Validation failed");

2. Include Request Context

Always include request ID, path, and method when available:

throw new HandlerError(
"Processing failed",
cause,
context.requestId, // For tracing
context.path, // For debugging
context.method, // For debugging
);

3. Provide Actionable Details

Include details that help diagnose the issue:

throw new ConfigurationError("Invalid rate limit configuration", {
field: "maxRequests",
provided: -100,
expected: "positive integer",
hint: "maxRequests must be greater than 0",
});

4. Respect Retry-After Headers

When handling RateLimitError, honor the retryAfterMs:

if (error instanceof RateLimitError) {
response.setHeader("Retry-After", Math.ceil(error.retryAfterMs / 1000));
}

5. Log Appropriately by Severity

switch (error.severity) {
case "CRITICAL":
logger.fatal(error);
alertOps(error);
break;
case "HIGH":
logger.error(error);
break;
case "MEDIUM":
logger.warn(error);
break;
case "LOW":
logger.info(error);
break;
}