Skip to main content
Beyond Sonamu’s built-in exception classes, you can create project-specific exceptions. Inherit from SoException to create exceptions with custom HTTP status codes and messages.

Basic Custom Exceptions

Simple Exception Class

// src/exceptions/payment-failed-exception.ts
import { SoException } from "sonamu";

export class PaymentFailedException extends SoException {
  constructor(
    public message = "Payment Failed",
    public payload?: unknown,
  ) {
    super(402, message, payload);  // 402 Payment Required
  }
}
Usage Example:
@api()
async processPayment(orderId: number, paymentMethod: string) {
  const order = await OrderModel.findById(orderId);

  try {
    const result = await paymentGateway.charge(
      order.amount,
      paymentMethod
    );

    if (!result.success) {
      throw new PaymentFailedException(
        "Payment failed",
        {
          orderId,
          reason: result.failureReason,
          errorCode: result.errorCode
        }
      );
    }

    return result;
  } catch (err) {
    throw new PaymentFailedException(
      "Error occurred during payment processing",
      { orderId, error: err }
    );
  }
}

403 Forbidden Exception

// src/exceptions/forbidden-exception.ts
import { SoException } from "sonamu";

export class ForbiddenException extends SoException {
  constructor(
    public message = "Forbidden",
    public payload?: unknown,
  ) {
    super(403, message, payload);
  }
}
Usage Example:
@api()
async accessRestrictedResource(ctx: Context, resourceId: number) {
  if (!ctx.user) {
    throw new UnauthorizedException("Login required");
  }

  const hasPermission = await PermissionModel.checkAccess(
    ctx.user.id,
    resourceId
  );

  if (!hasPermission) {
    throw new ForbiddenException(
      "You don't have permission to access this resource",
      { resourceId, userId: ctx.user.id }
    );
  }

  return this.getResource(resourceId);
}

Business Logic Exceptions

Domain-Specific Exceptions

// src/exceptions/inventory-exception.ts
import { SoException } from "sonamu";

export class OutOfStockException extends SoException {
  constructor(
    public message = "Out of Stock",
    public payload?: unknown,
  ) {
    super(550, message, payload);  // Custom status code
  }
}

export class InsufficientStockException extends SoException {
  constructor(
    public message = "Insufficient Stock",
    public payload?: unknown,
  ) {
    super(551, message, payload);
  }
}
Usage Example:
@api()
async createOrder(
  userId: number,
  items: Array<{ productId: number; quantity: number }>
) {
  for (const item of items) {
    const product = await ProductModel.findById(item.productId);

    if (!product) {
      throw new NotFoundException(
        `Product not found (ID: ${item.productId})`
      );
    }

    if (product.stock === 0) {
      throw new OutOfStockException(
        `${product.name} is out of stock`,
        {
          productId: product.id,
          productName: product.name
        }
      );
    }

    if (product.stock < item.quantity) {
      throw new InsufficientStockException(
        `Insufficient stock`,
        {
          productId: product.id,
          productName: product.name,
          requested: item.quantity,
          available: product.stock
        }
      );
    }
  }

  return this.createOrder(userId, items);
}

State Transition Exceptions

// src/exceptions/state-transition-exception.ts
import { SoException } from "sonamu";

export class InvalidStateTransitionException extends SoException {
  constructor(
    public message = "Invalid State Transition",
    public payload?: unknown,
  ) {
    super(560, message, payload);
  }
}
Usage Example:
@api()
async cancelOrder(orderId: number) {
  const order = await OrderModel.findById(orderId);

  if (!order) {
    throw new NotFoundException("Order not found");
  }

  // Check if cancellation is allowed
  const cancellableStates = ["pending", "confirmed"];

  if (!cancellableStates.includes(order.status)) {
    throw new InvalidStateTransitionException(
      "Cannot cancel order in current state",
      {
        orderId,
        currentState: order.status,
        allowedStates: cancellableStates,
        reason: this.getCancellationBlockReason(order.status)
      }
    );
  }

  return this.update(orderId, { status: "cancelled" });
}

private getCancellationBlockReason(status: string): string {
  const reasons = {
    shipping: "Cannot cancel order being shipped",
    delivered: "Cannot cancel delivered order",
    cancelled: "Order already cancelled"
  };
  return reasons[status] || "Cannot cancel in this state";
}

Limitation Exceptions

// src/exceptions/rate-limit-exception.ts
import { SoException } from "sonamu";

export class RateLimitExceededException extends SoException {
  constructor(
    public message = "Rate Limit Exceeded",
    public payload?: unknown,
  ) {
    super(429, message, payload);  // 429 Too Many Requests
  }
}

export class QuotaExceededException extends SoException {
  constructor(
    public message = "Quota Exceeded",
    public payload?: unknown,
  ) {
    super(570, message, payload);
  }
}
Usage Example:
@api()
async sendEmail(
  ctx: Context,
  to: string,
  subject: string,
  body: string
) {
  if (!ctx.user) {
    throw new UnauthorizedException("Login required");
  }

  // Check requests per minute
  const recentRequests = await this.getRecentRequestCount(
    ctx.user.id,
    60  // 60 seconds
  );

  if (recentRequests >= 10) {
    throw new RateLimitExceededException(
      "Exceeded email sending limit per minute",
      {
        limit: 10,
        window: "1 minute",
        retryAfter: 60
      }
    );
  }

  // Check daily quota
  const dailyCount = await this.getDailyEmailCount(ctx.user.id);
  const dailyLimit = ctx.user.plan === "premium" ? 1000 : 100;

  if (dailyCount >= dailyLimit) {
    throw new QuotaExceededException(
      "Exceeded daily email sending limit",
      {
        used: dailyCount,
        limit: dailyLimit,
        plan: ctx.user.plan,
        resetsAt: this.getNextResetTime()
      }
    );
  }

  return this.sendEmail(to, subject, body);
}

External Service Exceptions

External API Exceptions

// src/exceptions/external-service-exception.ts
import { SoException } from "sonamu";

export class ExternalServiceException extends SoException {
  constructor(
    public message = "External Service Error",
    public payload?: unknown,
  ) {
    super(502, message, payload);  // 502 Bad Gateway
  }
}

export class ExternalServiceTimeoutException extends SoException {
  constructor(
    public message = "External Service Timeout",
    public payload?: unknown,
  ) {
    super(504, message, payload);  // 504 Gateway Timeout
  }
}
Usage Example:
@api()
async getWeatherData(city: string) {
  try {
    const response = await axios.get(
      `https://api.weather.com/v1/current`,
      {
        params: { city },
        timeout: 5000
      }
    );

    return response.data;
  } catch (err) {
    if (axios.isAxiosError(err)) {
      if (err.code === "ECONNABORTED") {
        throw new ExternalServiceTimeoutException(
          "Weather API response timeout",
          { service: "weather-api", timeout: 5000 }
        );
      }

      throw new ExternalServiceException(
        "Failed to fetch weather data",
        {
          service: "weather-api",
          statusCode: err.response?.status,
          error: err.message
        }
      );
    }

    throw err;
  }
}

Exception Helper Classes

Exception Factory

// src/exceptions/exception-factory.ts
import { SoException } from "sonamu";

export class ValidationException extends SoException {
  static fieldRequired(fieldName: string) {
    return new ValidationException(
      `${fieldName} is required`,
      { field: fieldName, code: "REQUIRED" }
    );
  }

  static fieldInvalid(fieldName: string, reason: string) {
    return new ValidationException(
      `${fieldName} is invalid: ${reason}`,
      { field: fieldName, code: "INVALID", reason }
    );
  }

  static fieldTooShort(fieldName: string, minLength: number) {
    return new ValidationException(
      `${fieldName} must be at least ${minLength} characters`,
      { field: fieldName, code: "TOO_SHORT", minLength }
    );
  }

  constructor(message: string, payload?: unknown) {
    super(400, message, payload);
  }
}
Usage Example:
@api()
async createUser(username: string, password: string, email: string) {
  if (!username) {
    throw ValidationException.fieldRequired("username");
  }

  if (username.length < 3) {
    throw ValidationException.fieldTooShort("username", 3);
  }

  if (!email.includes("@")) {
    throw ValidationException.fieldInvalid(
      "email",
      "Not a valid email format"
    );
  }

  return this.create({ username, password, email });
}

Exception Inheritance Hierarchy

// src/exceptions/payment-exceptions.ts
import { SoException } from "sonamu";

// Base payment exception
export class PaymentException extends SoException {
  constructor(
    statusCode: number,
    message: string,
    payload?: unknown
  ) {
    super(statusCode, message, payload);
  }
}

// Specific payment exceptions
export class CardDeclinedException extends PaymentException {
  constructor(message?: string, payload?: unknown) {
    super(
      402,
      message || "Card declined",
      payload
    );
  }
}

export class InsufficientFundsException extends PaymentException {
  constructor(message?: string, payload?: unknown) {
    super(
      402,
      message || "Insufficient funds",
      payload
    );
  }
}

export class InvalidCardException extends PaymentException {
  constructor(message?: string, payload?: unknown) {
    super(
      402,
      message || "Invalid card",
      payload
    );
  }
}
Usage Example:
@api()
async processPayment(orderId: number, cardInfo: CardInfo) {
  try {
    const result = await paymentGateway.charge(cardInfo);
    return result;
  } catch (err) {
    if (err.code === "card_declined") {
      throw new CardDeclinedException(
        undefined,
        { reason: err.decline_reason }
      );
    }

    if (err.code === "insufficient_funds") {
      throw new InsufficientFundsException(
        undefined,
        { balance: err.available_balance }
      );
    }

    if (err.code === "invalid_card") {
      throw new InvalidCardException(
        undefined,
        { field: err.invalid_field }
      );
    }

    // Generic payment failure
    throw new PaymentException(
      402,
      "Error occurred during payment processing",
      { error: err }
    );
  }
}

Exception Grouping

Category-Based Exception Files

// src/exceptions/auth-exceptions.ts
export class SessionExpiredException extends SoException {
  constructor(message = "Session Expired", payload?: unknown) {
    super(401, message, payload);
  }
}

export class InvalidTokenException extends SoException {
  constructor(message = "Invalid Token", payload?: unknown) {
    super(401, message, payload);
  }
}

export class PasswordMismatchException extends SoException {
  constructor(message = "Password Mismatch", payload?: unknown) {
    super(401, message, payload);
  }
}
// src/exceptions/resource-exceptions.ts
export class ResourceLockedException extends SoException {
  constructor(message = "Resource Locked", payload?: unknown) {
    super(423, message, payload);  // 423 Locked
  }
}

export class ResourceConflictException extends SoException {
  constructor(message = "Resource Conflict", payload?: unknown) {
    super(409, message, payload);  // 409 Conflict
  }
}

Recommendations

HTTP Status Code Selection

Use standard HTTP status codes first, and use custom codes in the 5xx range when needed:
  • 400-499: Client errors (use standard codes)
  • 500-599: Server errors (540-599 for custom use)

Clear Messages

// ❌ Bad
throw new CustomException("Error");

// βœ… Good
throw new CustomException(
  "Cannot process order: insufficient stock",
  { productId: 123, available: 5, requested: 10 }
);

Structured Payload

// βœ… Consistent payload structure
throw new CustomException("Error message", {
  code: "ERROR_CODE",
  field: "fieldName",
  details: { /* additional info */ },
  suggestions: ["Solution 1", "Solution 2"]
});

Exception Documentation

/**
 * Exception thrown when stock is insufficient
 *
 * @example
 * ```typescript
 * throw new InsufficientStockException(
 *   "Insufficient stock",
 *   { productId: 123, available: 5, requested: 10 }
 * );
 * ```
 */
export class InsufficientStockException extends SoException {
  // ...
}