NextRush

@nextrush/errors

Standardized HTTP error classes with proper status codes

HTTP error classes for consistent error handling across your NextRush application.

$ pnpm add @nextrush/errors

Usually Included

Most users install nextrush which includes common error classes. Install @nextrush/errors directly for the full error hierarchy.

What It Provides

// Base classes
import { HttpError, NextRushError, getHttpStatusMessage } from '@nextrush/errors';

// 4xx Client Errors
import {
  BadRequestError, // 400
  UnauthorizedError, // 401
  ForbiddenError, // 403
  NotFoundError, // 404
  MethodNotAllowedError, // 405
  ConflictError, // 409
  UnprocessableEntityError, // 422
  TooManyRequestsError, // 429
  // ... and more
} from '@nextrush/errors';

// 5xx Server Errors
import {
  InternalServerError, // 500
  NotImplementedError, // 501
  BadGatewayError, // 502
  ServiceUnavailableError, // 503
  GatewayTimeoutError, // 504
  // ... and more
} from '@nextrush/errors';

// Validation errors
import {
  ValidationError,
  RequiredFieldError,
  TypeMismatchError,
  RangeValidationError,
  LengthError,
  PatternError,
  InvalidEmailError,
  InvalidUrlError,
} from '@nextrush/errors';

// Factory functions
import {
  createError,
  badRequest,
  unauthorized,
  forbidden,
  notFound,
  // ... and more
} from '@nextrush/errors';

// Middleware
import { errorHandler, notFoundHandler, catchAsync } from '@nextrush/errors';

Basic Usage

import { NotFoundError, BadRequestError } from '@nextrush/errors';

router.get('/users/:id', async (ctx) => {
  const user = await db.users.findById(ctx.params.id);

  if (!user) {
    throw new NotFoundError('User not found');
  }

  ctx.json(user);
});

router.post('/users', async (ctx) => {
  const { email } = ctx.body as { email?: string };

  if (!email) {
    throw new BadRequestError('Email is required');
  }

  // ...
});

Error Classes

Error Class Hierarchy

All NextRush errors extend NextRushError. All HTTP errors extend HttpError:

class NextRushError extends Error {
  readonly status: number;
  readonly code: string;
  readonly expose: boolean;
  readonly details?: Record<string, unknown>;
  readonly cause?: unknown;
}

class HttpError extends NextRushError {
  constructor(status: number, message?: string, options?: HttpErrorOptions);
}

NextRushError Properties

PropertyTypeDescription
statusnumber= 500HTTP status code
codestring= 'INTERNAL_ERROR'Machine-readable error code
exposeboolean= true for 4xx, false for 5xxWhether to expose message to client
details?Record<string, unknown>Additional structured data
cause?unknownOriginal error for error chaining

4xx Client Errors

ClassStatusDefault CodeUse Case
BadRequestError400BAD_REQUESTInvalid input
UnauthorizedError401UNAUTHORIZEDAuth required
PaymentRequiredError402PAYMENT_REQUIREDPayment needed
ForbiddenError403FORBIDDENAccess denied
NotFoundError404NOT_FOUNDResource missing
MethodNotAllowedError405METHOD_NOT_ALLOWEDWrong HTTP method
NotAcceptableError406NOT_ACCEPTABLECannot produce acceptable response
ProxyAuthRequiredError407PROXY_AUTH_REQUIREDProxy auth required
RequestTimeoutError408REQUEST_TIMEOUTClient took too long
ConflictError409CONFLICTState conflict
GoneError410GONEResource permanently deleted
LengthRequiredError411LENGTH_REQUIREDContent-Length required
PreconditionFailedError412PRECONDITION_FAILEDPrecondition not met
PayloadTooLargeError413PAYLOAD_TOO_LARGERequest body too large
UriTooLongError414URI_TOO_LONGRequest URI too long
UnsupportedMediaTypeError415UNSUPPORTED_MEDIA_TYPEContent type not supported
RangeNotSatisfiableError416RANGE_NOT_SATISFIABLECannot satisfy Range header
ExpectationFailedError417EXPECTATION_FAILEDCannot meet Expect header
ImATeapotError418IM_A_TEAPOTRFC 2324
UnprocessableEntityError422UNPROCESSABLE_ENTITYSemantic errors
LockedError423LOCKEDResource locked
FailedDependencyError424FAILED_DEPENDENCYDependent request failed
TooEarlyError425TOO_EARLYRequest replayed too early
UpgradeRequiredError426UPGRADE_REQUIREDClient should upgrade protocol
PreconditionRequiredError428PRECONDITION_REQUIREDConditional request required
TooManyRequestsError429TOO_MANY_REQUESTSRate limited
RequestHeaderFieldsTooLargeError431REQUEST_HEADER_FIELDS_TOO_LARGEHeaders too large
UnavailableForLegalReasonsError451UNAVAILABLE_FOR_LEGAL_REASONSLegal restriction

5xx Server Errors

ClassStatusDefault CodeUse Case
InternalServerError500INTERNAL_SERVER_ERRORUnexpected error
NotImplementedError501NOT_IMPLEMENTEDFeature missing
BadGatewayError502BAD_GATEWAYUpstream error
ServiceUnavailableError503SERVICE_UNAVAILABLETemporarily down
GatewayTimeoutError504GATEWAY_TIMEOUTUpstream timeout
HttpVersionNotSupportedError505HTTP_VERSION_NOT_SUPPORTEDHTTP version unsupported
VariantAlsoNegotiatesError506VARIANT_ALSO_NEGOTIATESContent negotiation error
InsufficientStorageError507INSUFFICIENT_STORAGEInsufficient storage
LoopDetectedError508LOOP_DETECTEDInfinite loop detected
NotExtendedError510NOT_EXTENDEDExtension not available
NetworkAuthRequiredError511NETWORK_AUTH_REQUIREDNetwork auth required

5xx Errors Don't Expose Messages

Server errors (5xx) have expose: false by default. In production, clients see "Internal Server Error" instead of the actual message.

Error Options

All error classes accept an options object:

HttpErrorOptions

PropertyTypeDescription
code?stringOverride default error code
expose?booleanOverride expose behavior
details?Record<string, unknown>Additional data attached to the error
cause?unknownOriginal error for error chaining

Examples

// Custom error code
throw new BadRequestError('Invalid email format', {
  code: 'INVALID_EMAIL',
});

// With details
throw new UnprocessableEntityError('Validation failed', {
  details: {
    fields: [
      { field: 'email', message: 'Invalid format' },
      { field: 'age', message: 'Must be positive' },
    ],
  },
});

// Wrapping original error
try {
  await database.query(sql);
} catch (err) {
  throw new InternalServerError('Database query failed', {
    cause: err,
  });
}

// Rate limit with retry-after
throw new TooManyRequestsError('Rate limit exceeded', {
  retryAfter: 60, // seconds
});

Validation Errors

ValidationError extends NextRushError (not HttpError) and provides structured validation failure reporting. It always returns status 400 with expose: true.

import {
  ValidationError,
  RequiredFieldError,
  TypeMismatchError,
  RangeValidationError,
  LengthError,
  PatternError,
  InvalidEmailError,
  InvalidUrlError,
} from '@nextrush/errors';

// Validation error with multiple issues
throw new ValidationError([
  { path: 'email', message: 'Required', rule: 'required' },
  { path: 'age', message: 'Must be positive', rule: 'range' },
]);

// Static factory methods
throw ValidationError.fromField('email', 'Invalid format', 'email');
throw ValidationError.fromFields({ email: 'Required', age: 'Must be number' });

// Field-specific errors
throw new RequiredFieldError('email');
throw new TypeMismatchError('age', 'number', 'string');
throw new RangeValidationError('age', 0, 120);
throw new LengthError('password', 8, 100);
throw new PatternError('username', '^[a-z0-9]+$');
throw new InvalidEmailError();
throw new InvalidUrlError();

ValidationError provides helper methods for querying issues:

const error = new ValidationError([
  { path: 'email', message: 'Required', rule: 'required' },
  { path: 'age', message: 'Must be positive', rule: 'range' },
]);

error.hasErrorFor('email'); // true
error.getErrorsFor('email'); // [{ path: 'email', ... }]
error.getFirstError('email'); // 'Required'
error.toFlatObject(); // { email: 'Required', age: 'Must be positive' }

Factory Functions

Create errors without new:

import {
  createError,
  badRequest,
  unauthorized,
  forbidden,
  notFound,
  conflict,
  tooManyRequests,
  internalError,
} from '@nextrush/errors';

// Generic factory
throw createError(400, 'Invalid input');
throw createError(404, 'User not found', { code: 'USER_NOT_FOUND' });

// Named factories
throw badRequest('Invalid email');
throw unauthorized('Token expired');
throw forbidden('Admin only');
throw notFound('User not found');
throw conflict('Email already exists');
throw tooManyRequests('Rate limit exceeded');
throw internalError('Database connection failed');

Error Utilities

isHttpError()

Type guard to check if an error is an HttpError:

import { isHttpError } from '@nextrush/errors';

try {
  await someOperation();
} catch (err) {
  if (isHttpError(err)) {
    console.log(err.status, err.code);
  } else {
    throw new InternalServerError('Unexpected error', { cause: err });
  }
}

getErrorStatus()

Get status code from any error:

import { getErrorStatus } from '@nextrush/errors';

const status = getErrorStatus(error); // 500 if not HttpError

getSafeErrorMessage()

Get error message safe for client exposure:

import { getSafeErrorMessage } from '@nextrush/errors';

const message = getSafeErrorMessage(error);
// Returns actual message if expose=true, otherwise generic message

Error Middleware

errorHandler()

Global error handling middleware. Place it before your routes so it catches all downstream errors.

import { errorHandler } from '@nextrush/errors';

// Basic usage (logs 5xx as errors, 4xx as warnings)
app.use(errorHandler());

// With options
app.use(
  errorHandler({
    // Include stack trace in responses (default: false)
    includeStack: process.env.NODE_ENV !== 'production',

    // Custom error logger
    logger: (err, ctx) => {
      myLogger.error(err, { path: ctx.path, method: ctx.method });
    },

    // Custom error response transformer
    transform: (err, ctx) => ({
      success: false,
      error: err.message,
      status: ctx.status,
    }),

    // Handle specific error types
    handlers: new Map([
      [
        SyntaxError,
        (err, ctx) => {
          ctx.status = 400;
          ctx.json({ error: 'Invalid JSON' });
        },
      ],
    ]),
  })
);

ErrorHandlerOptions

PropertyTypeDescription
includeStackboolean= falseInclude stack trace in error responses
logger(error: Error, ctx: Context) => voidCustom error logger. Default logs 5xx as errors and 4xx as warnings.
transform(error: Error, ctx: Context) => Record<string, unknown>Custom error response body transformer
handlersMap<ErrorClass, (error: Error, ctx: Context) => void>Custom handlers for specific error types

notFoundHandler()

Catch-all 404 handler. Responds with JSON when no route matched and ctx.status is still 404.

import { notFoundHandler } from '@nextrush/errors';

// Add after all routes
app.route('/', router);
app.use(notFoundHandler());

// With custom message
app.use(notFoundHandler('Route not found'));

catchAsync()

Deprecated

catchAsync() is redundant — async errors propagate through the middleware chain and are caught by errorHandler(). Use handlers directly.

// catchAsync is no longer needed. This works without it:
router.get('/users', async (ctx) => {
  const users = await db.users.findAll();
  ctx.json(users);
});

Complete Example

import { createApp } from '@nextrush/core';
import { createRouter } from '@nextrush/router';
import {
  errorHandler,
  notFoundHandler,
  NotFoundError,
  BadRequestError,
  ConflictError,
} from '@nextrush/errors';

const app = createApp();
const router = createRouter();

// Error handler first (catches all downstream errors)
app.use(
  errorHandler({
    includeStack: process.env.NODE_ENV !== 'production',
  })
);

// Routes
router.get('/users/:id', async (ctx) => {
  const user = await db.users.findById(ctx.params.id);

  if (!user) {
    throw new NotFoundError(`User ${ctx.params.id} not found`);
  }

  ctx.json(user);
});

router.post('/users', async (ctx) => {
  const { email, password } = ctx.body as Record<string, unknown>;

  if (!email || !password) {
    throw new BadRequestError('Email and password required');
  }

  const existing = await db.users.findByEmail(email);
  if (existing) {
    throw new ConflictError('Email already registered');
  }

  const user = await db.users.create({ email, password });
  ctx.status = 201;
  ctx.json(user);
});

app.route('/', router);

// 404 handler last
app.use(notFoundHandler());

TypeScript Types

import type {
  HttpErrorOptions,
  ValidationIssue,
  ErrorHandlerOptions,
  ErrorMiddleware,
  ErrorContext,
} from '@nextrush/errors';

See Also

On this page