Skip to main content
The @ooneex/exception package gives the framework one consistent way to fail. Instead of throwing bare Error objects, you throw an Exception — a subclass of the native Error that also carries an HTTP status code, an optional grouping key, immutable contextual data, the timestamp it was created, and (when you wrap a native error) the original error. Specialized subclasses like NotFoundException preset the status code so the most common HTTP failures read clearly at the throw site and map straight onto an error response.

Why this system

  • HTTP status mapping. Every exception carries a status code (default 500). Status codes come from @ooneex/http-status, so an exception maps directly onto the response it should produce.
  • Typed, immutable data. Attach a plain object of contextual data (the failing field, an entity id) at the throw site. It is frozen with Object.freeze, so the context that explains the failure cannot be mutated as the error propagates.
  • Built-in timestamp. Each exception records its creation date, so logs and error responses have an accurate moment of failure without extra bookkeeping.
  • JSON stack traces. stackToJson() parses the raw stack string into a structured ExceptionStackFrameType[] — function name, file, line, column — ready for logging or a debug-only response field.
  • Native error wrapping. Pass a caught Error straight into the constructor; its message is reused and the original is preserved on native, so you keep the underlying stack and context.
  • Specialized exceptions. BadRequestException, NotFoundException, UnauthorizedException, and MethodNotAllowedException preset the right status code so intent is obvious at the throw site.

How it works

Exception extends Error implements IException. The constructor takes the message (or an Error to wrap) and an options object; it reads status, key, and data, freezes data, sets name to the concrete class name, and — if the message was an Error — stores it on native. The date is captured the moment the instance is created.
MemberTypeDescription
keystring | nullOptional grouping key for the error (null by default).
dateDateTimestamp captured when the exception is constructed.
statusStatusCodeTypeHTTP status code; defaults to 500.
dataReadonly<Record<string, unknown>>Frozen contextual data attached at the throw site.
nativeError | undefinedThe original error, when a native Error was wrapped.
messagestringError message (inherited from Error).
namestringThe concrete class name, e.g. "NotFoundException".
stackstring | undefinedRaw stack trace string (inherited from Error).
stackToJson()ExceptionStackFrameType[] | nullParses stack into structured frames, or null when no stack exists.
The base constructor signature is:
new Exception(
  message: string | Error,
  options?: {
    key?: string | null;
    status?: StatusCodeType;
    data?: Record<string, unknown>;
  }
)
Each frame returned by stackToJson() matches ExceptionStackFrameType:
type ExceptionStackFrameType = {
  functionName?: string;
  fileName?: string;
  lineNumber?: number;
  columnNumber?: number;
  source: string;
};

The base Exception

Throw an Exception directly when no specialized type fits. With only a message it defaults to status 500 and empty data:
import { Exception } from "@ooneex/exception";

throw new Exception("Something went wrong");
Add a status code and typed contextual data — both are read straight off the instance later:
import { Exception } from "@ooneex/exception";
import { HttpStatus } from "@ooneex/http-status";

throw new Exception("Validation failed", {
  status: HttpStatus.Code.BadRequest,
  key: "user.email.invalid",
  data: {
    field: "email",
    value: "not-an-email",
    constraint: "Must be a valid email address",
  },
});
The data object is frozen, so reading it downstream is safe — attempting to mutate it has no effect.

Specialized exceptions

The specialized exceptions extend Exception and preset the status code from @ooneex/http-status. Their constructor is positional: a message, a required key string, and an optional data object (defaults to {}).
new NotFoundException(message: string, key: string, data?: Record<string, unknown>)
ExceptionStatus codeHttpStatus.Code
BadRequestException400BadRequest
UnauthorizedException401Unauthorized
NotFoundException404NotFound
MethodNotAllowedException405MethodNotAllowed
import {
  BadRequestException,
  MethodNotAllowedException,
  NotFoundException,
  UnauthorizedException,
} from "@ooneex/exception";

// 404 Not Found
throw new NotFoundException("User not found", "user.not_found", { userId: "123" });

// 400 Bad Request
throw new BadRequestException("Invalid input", "user.invalid", {
  errors: ["email is required", "name is too short"],
});

// 401 Unauthorized
throw new UnauthorizedException("Invalid credentials", "auth.invalid");

// 405 Method Not Allowed
throw new MethodNotAllowedException("POST is not allowed here", "route.method");

Throwing from a service or controller

Throw exceptions where the failure is detected — in a service or a controller — and let the calling layer turn them into a response. Because each exception carries its own status and data, the handler does not need to know how to classify the failure:
import { Exception, NotFoundException } from "@ooneex/exception";
import type { ContextType, IController } from "@ooneex/controller";

export class UserController implements IController {
  public async index(context: ContextType) {
    try {
      const user = await this.findUser(context.params.id);
      return context.response.json({ user });
    } catch (error) {
      if (error instanceof Exception) {
        // status and data travel with the exception
        return context.response.exception(error.message, {
          status: error.status,
          data: error.data,
        });
      }

      throw error;
    }
  }

  private async findUser(id: string) {
    const user = await db.users.find(id);
    if (!user) {
      throw new NotFoundException("User not found", "user.not_found", { id });
    }
    return user;
  }
}
The status and data you attached at the throw site flow straight into the error response. See Response for how context.response.exception(...) and the related helpers serialize this, and Controller for where this handling belongs.

Wrapping a native error

When a third-party call or a built-in throws a plain Error, wrap it instead of swallowing it. Pass the caught error as the message — the constructor reuses its message and keeps the original on native, so the underlying context is never lost:
import { Exception } from "@ooneex/exception";

try {
  JSON.parse(rawConfig);
} catch (error) {
  throw new Exception(error as Error, {
    status: 500,
    data: { context: "Parsing configuration file" },
  });
}
Downstream you can inspect both layers: exception.message and exception.data describe the application-level failure, while exception.native holds the original error and its stack.

Inspecting the stack as JSON

stackToJson() turns the raw stack string into structured frames you can log or filter, rather than a single opaque string:
import { Exception } from "@ooneex/exception";

try {
  throw new Exception("Test error");
} catch (error) {
  if (error instanceof Exception) {
    const frames = error.stackToJson();
    frames?.forEach((frame, index) => {
      console.log(`${index + 1}. ${frame.functionName ?? "<anonymous>"}`);
      console.log(`   at ${frame.fileName}:${frame.lineNumber}:${frame.columnNumber}`);
    });
  }
}

Best practices

  • Throw exceptions, not bare errors. Use Exception (or a specialized subclass) so every failure carries a status code and structured data that a handler can act on uniformly.
  • Pick the most specific type. Reach for NotFoundException, BadRequestException, UnauthorizedException, or MethodNotAllowedException when they fit; their preset status codes make intent obvious at the throw site.
  • Attach context as data, not in the message. Put ids, field names, and constraints in data so they stay machine-readable; keep the message human-readable.
  • Use key for grouping. A stable key lets you classify, route, and translate errors without parsing the message string.
  • Log with structure. Send exceptions to the Logger and use stackToJson() and data for searchable, structured records rather than concatenated strings.
  • Guard debug output. Expose stackToJson() only in non-production responses; in production surface message, status, and safe data only.
  • Wrap, don’t discard. When catching a native error, wrap it so its message and stack survive on native while you add application context.