GuidesError Handling

Usage

The foundation of error handling in Arkos is AppError — a structured error class that plugs into the global error handler. Every error you throw in a controller, interceptor, or service should be an AppError. Never send error responses manually with res.json() or res.status().json().

AppError

import { AppError } from "arkos/error-handler";

throw new AppError(message, statusCode, code?, meta?);
ParameterTypeRequiredDescription
messagestringYesHuman-readable error message shown to the client
statusCodenumberYesHTTP status code (400, 401, 403, 404, 409, 500...)
codestringNoMachine-readable code for client-side handling
metaobjectNoAdditional context — avoid sensitive data
throw new AppError(
  "Email already registered",
  409,
  "EmailAlreadyExists",
  { email: req.body.email }
);

Response:

{
  "status": "fail",
  "message": "Email already registered",
  "code": "EmailAlreadyExists",
  "meta": {
    "email": "user@example.com"
  }
}

Always Throw, Never Respond

The single most important practice: throw AppError instead of writing custom error responses. Manual responses bypass the global error handler, break response consistency, skip onError interceptors, and won't be included in future OpenAPI error documentation.

Don't do this:

import { BaseController } from "arkos/controllers";
import { ArkosRequest, ArkosResponse } from "arkos";
import { AppError } from "arkos/error-handler";
import userService from "./user.service";

class UserController extends BaseController {
  getUser = async (req: ArkosRequest, res: ArkosResponse) => {
    const user = await userService.findOne({ id: req.params.id });

    if (!user) {
      return res.status(404).json({ error: "User not found" }); // ❌
    }

    res.json({ data: user });
  };
}

export default new UserController();

Do this:

import { BaseController } from "arkos/controllers";
import { ArkosRequest, ArkosResponse } from "arkos";
import { AppError } from "arkos/error-handler";
import userService from "./user.service";

class UserController extends BaseController {
  getUser = async (req: ArkosRequest, res: ArkosResponse) => {
    const user = await userService.findOne({ id: req.params.id });

    if (!user) {
      throw new AppError(`User not found with id: ${req.params.id}`, 404, "NotFound"); // ✅
    }

    res.json({ data: user });
  };
}

export default new UserController(userService);

Because ArkosRouter wraps all handlers with catchAsync automatically, you never need to wrap handlers or call next(err) manually for thrown errors — just throw.

In-Route Error Handlers

For cases where you need to catch an error locally — for instance to add context before re-throwing, or to handle a third-party call that throws its own error type — use a standard Express error handler (4-param middleware) registered on the route itself. Always rethrow as AppError so the global handler receives a normalized error:

src/modules/payment/payment.router.ts
import { ArkosRouter } from "arkos";
import { AppError } from "arkos/error-handler";
import paymentController from "./payment.controller";
import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos";

const paymentRouter = ArkosRouter();

paymentRouter.post(
  { path: "/api/payments" },
  paymentController.charge,
  // in-route error handler — catches errors from paymentController.charge
  (err: any, req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => {
    // Normalize third-party Stripe errors into AppError before forwarding
    if (err?.type === "StripeCardError") {
      return next(new AppError(err.message, 402, "PaymentFailed", { declineCode: err.decline_code }));
    }
    next(err); // anything else goes straight to the global handler
  }
);

export default paymentRouter;

In-route error handlers must have exactly 4 parameters (err, req, res, next) — Express uses the parameter count to identify them as error handlers. See Express error handling docs for more.

In-Route Error Handling For Built-in Routes

src/modules/post/post.interceptors.ts
// For built-in routes, use interceptor error handlers instead of in-route handlers
export const onCreateOneError = [
  async (err: any, req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => {
    if (req.uploadedImageUrl) await deleteFromS3(req.uploadedImageUrl);
    next(err);
  },
];

Read more at Interceptors — Handling Errors

In-Line Error Handling For Service Layer

src/modules/post/post.hooks.ts
// For service-level errors
export const onCreateOneError = [
  async ({ error, data }) => {
    console.error("Service error:", error.message);
  },
];

Read more at Service Hooks — Handling Errors

Common Status Codes

CodeWhen to use
400Invalid input, bad request
401Not authenticated
403Authenticated but not authorized
404Resource not found
409Conflict — duplicate entry, constraint violation
500Unexpected server error

Error Hooks

If you need to run cleanup or rollback logic when an operation fails — uploaded files, reserved inventory, database transactions — use onError interceptors or service hook error handlers rather than try/catch blocks in your controllers: