Skip to main content
Learn how to consistently handle errors in APIs and provide clear responses.

Error Handling Overview

Consistent Responses

Standardized error format Client-friendly

Status Codes

HTTP status codes RESTful standard

Clear Messages

Developer-friendly User-friendly

Logging

Error tracking Debugging support

SoException

Sonamu provides error classes based on SoException. When you throw a SoException, the framework automatically sends an appropriate HTTP status code and structured error response.

Basic Usage

import { NotFoundException } from "sonamu";

class UserModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async get(id: number): Promise<User> {
    const rdb = this.getPuri("r");

    const user = await rdb.table("users").where("id", id).first();

    if (!user) {
      throw new NotFoundException("User not found");
    }

    return user;
  }
}

Available Exception Classes

These are the exception classes provided by Sonamu. All inherit from SoException.
ClassStatus CodeWhen to Use
BadRequestException400Invalid parameters or malformed request
UnauthorizedException401Authentication required or access denied
NotFoundException404Accessing a non-existent record
InternalServerErrorException500Internal processing or external API call error
ServiceUnavailableException503Processing not possible in current state
TargetNotFoundException520Processing target does not exist
AlreadyProcessedException541Request has already been processed
DuplicateRowException542Duplicate request where duplicates are not allowed

SoException Constructor

All exception classes share the same constructor signature.
constructor(message: LocalizedString, payload?: unknown)
  • message: Error message. Supports localized strings (LocalizedString).
  • payload: Additional information. Can pass Zod validation issue arrays, etc.

Usage Examples

Various Error Situations

import {
  BadRequestException,
  UnauthorizedException,
  NotFoundException,
  DuplicateRowException,
} from "sonamu";

class UserModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async get(id: number): Promise<User> {
    const rdb = this.getPuri("r");

    const user = await rdb.table("users").where("id", id).first();

    if (!user) {
      throw new NotFoundException("User not found");
    }

    return user;
  }

  @api({ httpMethod: "POST" })
  async create(params: CreateUserParams): Promise<{ userId: number }> {
    const context = Sonamu.getContext();

    // Check authentication
    if (!context.user) {
      throw new UnauthorizedException("Authentication required");
    }

    // Input validation
    if (!params.email) {
      throw new BadRequestException("Email is required");
    }

    const rdb = this.getPuri("r");

    // Check duplicate
    const existing = await rdb.table("users").where("email", params.email).first();

    if (existing) {
      throw new DuplicateRowException("Email already exists");
    }

    const wdb = this.getPuri("w");
    const [user] = await wdb.table("users").insert(params).returning({ id: "id" });

    return { userId: user.id };
  }
}

Passing Detailed Info via payload

import { BadRequestException } from "sonamu";

class OrderModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async create(params: CreateOrderParams): Promise<{ orderId: number }> {
    // Pass error details via payload
    if (params.quantity <= 0) {
      throw new BadRequestException("Invalid order quantity", {
        field: "quantity",
        value: params.quantity,
        constraint: "must be > 0",
      });
    }

    // ...
  }
}

isSoException Type Guard

import { isSoException, AlreadyProcessedException } from "sonamu";

class PaymentModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async process(paymentId: number): Promise<void> {
    try {
      await this.executePayment(paymentId);
    } catch (error) {
      if (isSoException(error) && error.statusCode === 541) {
        // Already processed payment - ignore
        return;
      }
      throw error;
    }
  }
}

Zod Validation Error Handling

When you pass a Zod issue array to BadRequestException’s payload, Sonamu’s error handler automatically includes validation error details in the response.

Converting Zod Errors

import { z } from "zod";
import { BadRequestException } from "sonamu";

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2),
  password: z.string().min(8),
});

class UserModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async create(params: unknown): Promise<{ userId: number }> {
    const result = CreateUserSchema.safeParse(params);

    if (!result.success) {
      throw new BadRequestException("Validation failed", result.error.issues);
    }

    const validated = result.data;

    const wdb = this.getPuri("w");
    const [user] = await wdb.table("users").insert(validated).returning({ id: "id" });

    return { userId: user.id };
  }
}

Error Response Format

Sonamu’s built-in error handler responds in the following format.

Basic Error Response

{
  "name": "NotFoundException",
  "code": null,
  "message": "User not found"
}

Zod Validation Error Response (when issue array is passed as payload)

{
  "name": "BadRequestException",
  "code": null,
  "message": "Validation failed (email)",
  "issues": [
    {
      "code": "invalid_type",
      "expected": "string",
      "received": "undefined",
      "path": ["email"],
      "message": "Required"
    }
  ]
}

Customizing the Error Handler

Sonamu includes a built-in error handler, but you can set a custom error handler via the lifecycle.onError option in startServer if needed.
import { Sonamu, isSoException } from "sonamu";

Sonamu.startServer({
  lifecycle: {
    onError: (error, request, reply) => {
      // Custom error handling logic
      if (isSoException(error)) {
        reply.status(error.statusCode).send({
          error: error.message,
          payload: error.payload,
        });
      } else {
        reply.status(500).send({
          error: "Internal server error",
        });
      }
    },
  },
});

Status Code Reference

Standard HTTP Status Codes

CodeNameWhen to Use
200OKSuccess (GET, PUT)
201CreatedResource created (POST)
204No ContentSuccess, no response (DELETE)
400Bad RequestInvalid request format
401UnauthorizedAuthentication required
404Not FoundResource not found
500Internal Server ErrorInternal server error
503Service UnavailableService unavailable

Sonamu Custom Status Codes

CodeException ClassWhen to Use
520TargetNotFoundExceptionProcessing target does not exist
541AlreadyProcessedExceptionRequest already processed
542DuplicateRowExceptionDuplicate where duplicates not allowed

Practical Pattern

import {
  UnauthorizedException,
  BadRequestException,
  DuplicateRowException,
} from "sonamu";

class UserModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async create(params: unknown): Promise<{ userId: number }> {
    const context = Sonamu.getContext();

    // 1. Check authentication
    if (!context.user) {
      throw new UnauthorizedException("Authentication required");
    }

    // 2. Zod validation
    const validated = CreateUserSchema.safeParse(params);
    if (!validated.success) {
      throw new BadRequestException("Validation failed", validated.error.issues);
    }

    const data = validated.data;

    // 3. Business rule validation
    const rdb = this.getPuri("r");
    const existing = await rdb.table("users").where("email", data.email).first();

    if (existing) {
      throw new DuplicateRowException("Email already exists");
    }

    // 4. Create
    const wdb = this.getPuri("w");
    const [user] = await wdb.table("users").insert(data).returning({ id: "id" });

    return { userId: user.id };
  }
}

Cautions

Cautions for error handling: 1. Never expose sensitive information (stack traces, DB errors, etc.) 2. Use appropriate HTTP status codes 3. Provide clear and consistent error messages 4. Always log errors 5. Limit detailed information in production

Next Steps

Automatic Validation

Zod-based validation

Custom Validation

Custom validation logic

@api Decorator

API basic usage

Context

SonamuContext