api-errors

McpError constructor, JsonRpcErrorCode reference, and error handling patterns for `@cyanheads/mcp-ts-core`. Use when looking up error codes, understanding where errors should be thrown vs. caught, or using ErrorHandler.tryCatch in services.

Overview

Error handling in @cyanheads/mcp-ts-core follows a strict layered pattern: tool and resource handlers throw McpError freely (no try/catch), the handler factory catches and normalizes all errors, and services use ErrorHandler.tryCatch for graceful recovery.

Imports:

import {
  notFound,
  validationError,
  McpError,
  JsonRpcErrorCode,
} from "@cyanheads/mcp-ts-core/errors";
import { ErrorHandler } from "@cyanheads/mcp-ts-core/utils";

Error Factories (Preferred)

Shorter than new McpError(...) and self-documenting. All return McpError instances. All accept an optional options parameter for error chaining via { cause }.

throw notFound("Item not found", { itemId: "123" });
throw validationError("Missing required field: name", { field: "name" });
throw unauthorized("Token expired");

// With cause for error chaining
throw serviceUnavailable("API call failed", { url }, { cause: error });

Available factories:

FactoryCode
invalidParams(msg, data?, options?)InvalidParams (-32602)
invalidRequest(msg, data?, options?)InvalidRequest (-32600)
notFound(msg, data?, options?)NotFound (-32001)
forbidden(msg, data?, options?)Forbidden (-32005)
unauthorized(msg, data?, options?)Unauthorized (-32006)
validationError(msg, data?, options?)ValidationError (-32007)
conflict(msg, data?, options?)Conflict (-32002)
rateLimited(msg, data?, options?)RateLimited (-32003)
timeout(msg, data?, options?)Timeout (-32004)
serviceUnavailable(msg, data?, options?)ServiceUnavailable (-32000)
configurationError(msg, data?, options?)ConfigurationError (-32008)

options is { cause?: unknown } — the standard ES2022 ErrorOptions type.


McpError Constructor

For codes not covered by factories (InternalError, DatabaseError, etc.):

throw new McpError(code, message?, data?, options?)
  • code — a JsonRpcErrorCode enum value
  • message — optional human-readable description of the failure
  • data — optional structured context (plain object)
  • options — optional { cause?: unknown } for error chaining

Example:

import { McpError, JsonRpcErrorCode } from "@cyanheads/mcp-ts-core/errors";

throw new McpError(
  JsonRpcErrorCode.DatabaseError,
  "Connection pool exhausted",
  {
    pool: "primary",
  },
);

Error Codes

Standard JSON-RPC 2.0 codes:

CodeValueWhen to Use
ParseError-32700Malformed JSON received
InvalidRequest-32600Unsupported operation, missing client capability
MethodNotFound-32601Requested method does not exist
InvalidParams-32602Bad input, missing required fields, schema validation failure
InternalError-32603Unexpected failure, catch-all for programmer errors

Implementation-defined codes (-32000 to -32099):

CodeValueWhen to Use
ServiceUnavailable-32000External dependency down, upstream failure
NotFound-32001Resource, entity, or record doesn't exist
Conflict-32002Duplicate key, version mismatch, concurrent modification
RateLimited-32003Rate limit exceeded
Timeout-32004Operation exceeded time limit
Forbidden-32005Authenticated but insufficient scopes/permissions
Unauthorized-32006No auth, invalid token, expired credentials
ValidationError-32007Business rule violation (not schema — use InvalidParams for that)
ConfigurationError-32008Missing env var, invalid config
InitializationFailed-32009Server/component startup failure
DatabaseError-32010Storage/persistence layer failure
SerializationError-32070Data serialization/deserialization failed
UnknownError-32099Generic fallback when no other code fits

Auto-Classification

When a handler throws a plain Error (or any non-McpError value), the framework classifies it to the most specific JsonRpcErrorCode automatically. This matters when you don't control what a third-party library throws and can't predict its error type.

Use factories or McpError directly when the code must be exact — auto-classification is best-effort pattern matching and not guaranteed for ambiguous messages. For errors from your own code where the code matters, be explicit.

Resolution Order

The framework applies these steps in order — first match wins:

  1. McpError instanceerror.code is preserved as-is; no classification needed.
  2. JS constructor name — matched against a fixed table (e.g. TypeErrorValidationError).
  3. Provider-specific patterns — HTTP status codes, AWS exception names, Supabase, OpenRouter. Checked before common patterns because they are more specific (e.g. status code 429 beats the generic rate limit pattern).
  4. Common message/name patterns — broad keyword patterns covering auth, not-found, validation, etc. First match wins; order matters.
  5. AbortError nameerror.name === 'AbortError'Timeout.
  6. FallbackInternalError.

JS Constructor Name Mappings

ConstructorMapped Code
SyntaxErrorValidationError
TypeErrorValidationError
RangeErrorValidationError
URIErrorValidationError
ReferenceErrorInternalError
EvalErrorInternalError
AggregateErrorInternalError

Common Message Patterns

Patterns are tested against both the error message and name, case-insensitively. First match wins.

Pattern (regex)Mapped Code
unauthorized|unauthenticated|not\s+authorized|not.*logged.*in|invalid[\s_-]+token|expired[\s_-]+tokenUnauthorized
permission|forbidden|access.*denied|not.*allowedForbidden
not found|no such|doesn't exist|couldn't findNotFound
invalid|validation|malformed|bad request|wrong format|missing\s+(?:required|param|field|input|value|arg)ValidationError
conflict|already exists|duplicate|unique constraintConflict
rate limit|too many requests|throttledRateLimited
timeout|timed out|deadline exceededTimeout
abort(ed)?|cancell?edTimeout
service unavailable|bad gateway|gateway timeout|upstream errorServiceUnavailable
zod|zoderror|schema validationValidationError

Provider-Specific Patterns

Checked before common patterns. Cover: AWS exception names, HTTP status codes, DB connection/constraint errors, Supabase JWT/RLS, OpenRouter/LLM quota errors, and low-level network errors.

PatternMapped Code
ThrottlingException|TooManyRequestsExceptionRateLimited
AccessDenied|UnauthorizedOperationForbidden
ResourceNotFoundExceptionNotFound
status code 401Unauthorized
status code 403Forbidden
status code 404NotFound
status code 409Conflict
status code 429RateLimited
status code 5xxServiceUnavailable
ECONNREFUSED|connection refusedServiceUnavailable
ETIMEDOUT|connection timeoutTimeout
unique constraint|duplicate keyConflict
foreign key constraintValidationError
JWT expiredUnauthorized
row level securityForbidden
insufficient_quota|quota exceededRateLimited
model_not_foundNotFound
context_length_exceededValidationError
ENOTFOUND|DNSServiceUnavailable
ECONNRESET|connection resetServiceUnavailable

Where Errors Are Handled

LayerPattern
Tool/resource handlersThrow McpError — no try/catch
Handler factoryCatches all errors, normalizes to McpError, sets isError: true
Services/setup codeErrorHandler.tryCatch for graceful recovery

Handler — throw freely, no try/catch:

import { notFound } from "@cyanheads/mcp-ts-core/errors";

export const myTool = tool("my_tool", {
  input: z.object({ id: z.string().describe("Item ID") }),
  output: z.object({ id: z.string(), name: z.string(), status: z.string() }),
  async handler(input, ctx) {
    const item = await db.find(input.id);
    if (!item) {
      throw notFound(`Item not found: ${input.id}`, { id: input.id });
    }
    return item;
  },
});

ErrorHandler.tryCatch (Services)

Use ErrorHandler.tryCatch in service code, not in tool handlers. It wraps arbitrary exceptions into McpError and supports structured logging context.

import { ErrorHandler } from "@cyanheads/mcp-ts-core/utils";

// Works with both async and sync functions
const result = await ErrorHandler.tryCatch(() => externalApi.fetch(url), {
  operation: "ExternalApi.fetch",
  context: { url },
  errorCode: JsonRpcErrorCode.ServiceUnavailable,
});

const parsed = await ErrorHandler.tryCatch(() => JSON.parse(raw), {
  operation: "parseConfig",
  errorCode: JsonRpcErrorCode.ConfigurationError,
});

tryCatch always logs and rethrows — it never swallows errors. The fn argument may be synchronous or return a Promise; both are handled via Promise.resolve(fn()).

Options (Omit<ErrorHandlerOptions, 'rethrow'>):

OptionTypeRequiredPurpose
operationstringYesName logged with the error
contextErrorContextNoExtra structured fields merged into the log record; requestId and timestamp receive special treatment
errorCodeJsonRpcErrorCodeNoCode used if the caught error is not already an McpError
inputunknownNoInput value sanitized and logged alongside the error
criticalbooleanNoMarks the error as critical in logs (default false)
includeStackbooleanNoInclude stack trace in log output (default true)
errorMapper(error: unknown) => ErrorNoCustom transform applied instead of default McpError wrapping