ExisJS

Error Handling

ExisJS implements a comprehensive error boundary and observability system. The framework is designed to reduce the need to manually construct HTTP error responses.

The HttpError System

You can throw an HttpError anywhere in your application, and the ExisJS router will automatically intercept it, log it, and format it into a standardized JSON response for the client.

Functional Paradigm

src/http/users/route.ts
import { controller, route } from 'exisjs/router'import { HttpError } from 'exisjs/error'
export default controller({  getUser: route.get('/:id', {    async handle({ params }) {      const user = db.findUser(params.id)            if (!user) {        // This stops execution and returns a 404        throw HttpError.notFound('User')       }            if (user.isBanned) {        // This returns a 403 Forbidden        throw HttpError.forbidden('User is banned from the platform')      }
      return { user }    }  })})

Class-Based (OOP) Paradigm

src/http/users/route.ts
import { Controller, Get, Param } from 'exisjs/decorators'import { HttpError } from 'exisjs/error'
@Controller()export default class UsersController {  @Get('/:id')  async getUser(@Param('id') id: string) {    const user = db.findUser(id)        if (!user) {      // This stops execution and returns a 404      throw HttpError.notFound('User')     }        if (user.isBanned) {      // This returns a 403 Forbidden      throw HttpError.forbidden('User is banned from the platform')    }
    return { user }  }}

Common Error Factories

The HttpError class provides static factory methods for standard HTTP status codes:

  • HttpError.badRequest('Message') - 400
  • HttpError.unauthorized('Message') - 401
  • HttpError.forbidden('Message') - 403
  • HttpError.notFound('Resource') - 404
  • HttpError.conflict('Message') - 409
  • HttpError.unprocessable('Message', details) - 422
  • HttpError.internal('Message') - 500

Direct Error Classes & Exceptions

If you prefer a class-based approach, ExisJS exports every standard error as its own class. We also provide Exception aliases for every error class. You can import and throw them directly:

example.ts
import { NotFoundException, UnauthorizedException, BadRequestError } from 'exisjs/error'
export const deleteBook = (req, res) => {  const book = db.findBook(req.params.id)    if (!book) throw new NotFoundException('Book not found')  if (book.ownerId !== req.user.id) throw new UnauthorizedException('Unauthorized')      // ...}

The Standardized Response

When an HttpError is thrown, the client will automatically receive a structured JSON payload.

Response
{  "success": false,  "error": {    "code": "NOT_FOUND",    "message": "User not found"  }}

If you throw an unhandled exception (like a database connection failure or a TypeError), ExisJS catches it and securely returns a 500 Internal Server Error, hiding the stack trace from the client in production mode while logging the full trace to your internal logger.

Error Handling Decision Tree

To keep your codebase clean and consistent, use this decision tree when deciding how to handle errors:

  1. Is it a known client error (e.g., missing parameter, forbidden)? šŸ‘‰ Throw an HttpError directly. Let the framework format the 4xx response automatically.
  2. Do you need to intercept an unexpected failure (e.g., Database Connection failure) globally? šŸ‘‰ Use an Exception Filter (catchError) in server.ts. Map the specific exception class to a 5xx response.
  3. Do you need to intercept errors only for a specific route group (e.g., Admin Panel)? šŸ‘‰ Use Local Error Hooks (onError) in the gateway/controller. Keep the logic isolated to that specific module.
  4. Is it standard 4-arg middleware from the Express ecosystem? šŸ‘‰ Use app.use(errorHandler) (where errorHandler = (err, req, res, next) => {}). ExisJS supports standard Connect/Express error middleware natively.

Extended Error Handling

ExisJS provides mechanisms for granular error handling.

1. Exception Filters (catchError)

ExisJS allows you to create Exception Filters to globally intercept specific types of errors.

Using the catchError middleware, you can map specific custom errors to custom responses globally:

Functional Paradigm

src/http/server.ts
import { exis } from 'exisjs'import { catchError } from 'exisjs/middleware'
export class DatabaseConnectionError extends Error {  constructor(message: string) {    super(message)    this.name = 'DatabaseConnectionError'  }}
export default exis({  async onStart(app) {    // Globally intercept DatabaseConnectionError    app.use(catchError(DatabaseConnectionError, (err, req, res) => {      res.status(503).json({        success: false,        error: { code: 'DB_OFFLINE', message: err.message }      })    }))  }})

Class-Based (OOP) Paradigm

src/http/server.ts
import { Server } from 'exisjs/decorators'import { catchError } from 'exisjs/middleware'import type { App } from 'exisjs'
export class DatabaseConnectionError extends Error {  constructor(message: string) {    super(message)    this.name = 'DatabaseConnectionError'  }}
@Server()export default class RootServer {  async onStart(app: App) {    // Globally intercept DatabaseConnectionError    app.use(catchError(DatabaseConnectionError, (err, req, res) => {      res.status(503).json({        success: false,        error: { code: 'DB_OFFLINE', message: err.message }      })    }))  }}

This prevents you from having to write try/catch blocks in every single route just to handle standard database failures.

2. Local Error Hooks (onError)

If you want to catch errors only within a specific module or route grouping, you can use the onError hook directly in your controller definition.

When a controller throws an error, the local onError hook will intercept it before it reaches the global error handler.

Functional Paradigm

src/http/users/route.ts
import { controller, route } from 'exisjs/router'
export default controller({  // This local hook catches errors thrown by the routes in this controller  onError: (err, req, res) => {    console.log('Users Controller Error Handler Caught:', err.message)    res.status(400).json({ success: false, localError: err.message })  },
  crash: route.get('/crash', async () => {    // This error will be caught by the local onError hook    throw new Error("Something went wrong in the users module")  })})

Class-Based (OOP) Paradigm

src/http/users/route.ts
import { Controller, Get, UseFilters } from 'exisjs/decorators'import type { Request, Response } from 'exisjs/router'
function localErrorHandler(err: any, req: Request, res: Response, next: any) {  console.log('Users Controller Error Handler Caught:', err.message)  res.status(400).json({ success: false, localError: err.message })}
@Controller()@UseFilters(localErrorHandler)export default class UsersController {  @Get('/crash')  async crash() {    throw new Error("Something went wrong in the users module")  }}

By combining Exception Filters and Local Error Hooks, ExisJS gives you control over your error boundaries.

Observability (BYOM/BYOT)

ExisJS ships with tracing and metric adapters. Instead of tying you to a specific telemetry ecosystem, ExisJS lets you Bring Your Own Metrics (BYOM) and Bring Your Own Tracer (BYOT).

Metrics

You can use the metrics middleware to track request durations and counts.

Functional Paradigm

src/http/gateway.ts
import { defineGateway } from 'exisjs/router'import { metrics } from 'exisjs/observability'
const myPrometheusAdapter = {  onRequestStart: ({ method, path }) => {    // A request just started. Start a timer.    return ({ statusCode, durationMs }) => {      // The request just finished. Push to Prometheus.      console.log(`[Metrics] ${method} ${path} took ${durationMs}ms with status ${statusCode}`)    }  }}
export default defineGateway({  middleware: [metrics(myPrometheusAdapter)]})

Class-Based (OOP) Paradigm

src/http/gateway.ts
import { Gateway } from 'exisjs/decorators'import { metrics } from 'exisjs/observability'
const myPrometheusAdapter = {  onRequestStart: ({ method, path }) => {    return ({ statusCode, durationMs }) => {      console.log(`[Metrics] ${method} ${path} took ${durationMs}ms with status ${statusCode}`)    }  }}
@Gateway({  middleware: [metrics(myPrometheusAdapter)]})export default class RootGateway {}

Note: ExisJS normalizes dynamic paths (e.g., turning /users/123 into /users/:id) to prevent metrics cardinality issues.

OpenTelemetry Tracing

ExisJS seamlessly integrates with OpenTelemetry (OTel) via the tracing middleware.

Functional Paradigm

src/http/gateway.ts
import { defineGateway } from 'exisjs/router'import { tracing } from 'exisjs/observability'
const myOtelAdapter = {  startActiveSpan: (name, metadata, callback) => {    // Create an OTel span    const span = tracer.startSpan(name)    // Run the callback and pass the span proxy    callback({      setAttribute: (k, v) => span.setAttribute(k, v),      setStatus: (status) => span.setStatus(status),      recordException: (e) => span.recordException(e),      end: () => span.end()    })  }}
export default defineGateway({  middleware: [tracing(myOtelAdapter)]})

Class-Based (OOP) Paradigm

src/http/gateway.ts
import { Gateway } from 'exisjs/decorators'import { tracing } from 'exisjs/observability'
const myOtelAdapter = {  startActiveSpan: (name, metadata, callback) => {    const span = tracer.startSpan(name)    callback({      setAttribute: (k, v) => span.setAttribute(k, v),      setStatus: (status) => span.setStatus(status),      recordException: (e) => span.recordException(e),      end: () => span.end()    })  }}
@Gateway({  middleware: [tracing(myOtelAdapter)]})export default class RootGateway {}

By providing these standard interfaces natively, your application metrics and traces are bound to the ExisJS router lifecycle.