SoException to create exceptions with custom HTTP status codes and messages.
Basic Custom Exceptions
Simple Exception Class
Copy
// 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
}
}
Copy
@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
Copy
// 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);
}
}
Copy
@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
Copy
// 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);
}
}
Copy
@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
Copy
// 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);
}
}
Copy
@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
Copy
// 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);
}
}
Copy
@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
Copy
// 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
}
}
Copy
@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
Copy
// 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);
}
}
Copy
@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
Copy
// 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
);
}
}
Copy
@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
Copy
// 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);
}
}
Copy
// 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
Copy
// β Bad
throw new CustomException("Error");
// β
Good
throw new CustomException(
"Cannot process order: insufficient stock",
{ productId: 123, available: 5, requested: 10 }
);
Structured Payload
Copy
// β
Consistent payload structure
throw new CustomException("Error message", {
code: "ERROR_CODE",
field: "fieldName",
details: { /* additional info */ },
suggestions: ["Solution 1", "Solution 2"]
});
Exception Documentation
Copy
/**
* Exception thrown when stock is insufficient
*
* @example
* ```typescript
* throw new InsufficientStockException(
* "Insufficient stock",
* { productId: 123, available: 5, requested: 10 }
* );
* ```
*/
export class InsufficientStockException extends SoException {
// ...
}