Skip to main content
SoException is the abstract base class that all Sonamu exception classes inherit from. It can include HTTP status codes, messages, and additional payloads.

Type Definition

abstract class SoException extends Error {
  constructor(
    public readonly statusCode: number,
    public message: string,
    public payload?: unknown,
  );
}

Properties

statusCode

readonly statusCode: number
HTTP response status code. When an exception is thrown, Fastify responds with this code.

message

message: string
Exception message. This is the error description delivered to the client.

payload

payload?: unknown
Optional additional data. Used to convey detailed information related to the exception. Usage Example:
throw new BadRequestException("Invalid email format", {
  field: "email",
  providedValue: "invalid-email",
  expectedFormat: "user@example.com"
});

Utility Functions

isSoException()

Checks whether a given value is an instance of SoException.
function isSoException(err: unknown): err is SoException
Usage Example:
try {
  // Some operation
} catch (err) {
  if (isSoException(err)) {
    console.log(`Status: ${err.statusCode}`);
    console.log(`Message: ${err.message}`);
    console.log(`Payload:`, err.payload);
  } else {
    // Handle other error types
  }
}

Built-in Exception Classes

Sonamu provides several built-in exception classes for common HTTP error scenarios.

BadRequestException (400)

Used when there are issues with request parameters or other request details.
class BadRequestException extends SoException
Usage Example:
@api()
async createUser(email: string, password: string) {
  if (!email.includes("@")) {
    throw new BadRequestException("Invalid email format");
  }

  if (password.length < 8) {
    throw new BadRequestException("Password must be at least 8 characters", {
      minLength: 8,
      providedLength: password.length
    });
  }

  // User creation logic
}

UnauthorizedException (401)

Used when login is required but the user is logged out, or when access permission is lacking.
class UnauthorizedException extends SoException
Usage Example:
@api()
async getMyProfile(ctx: Context) {
  if (!ctx.user) {
    throw new UnauthorizedException("Login required");
  }

  return this.findById(ctx.user.id);
}

NotFoundException (404)

Used when accessing a non-existent record.
class NotFoundException extends SoException
Usage Example:
@api()
async getUserById(id: number) {
  const user = await this.findById(id);

  if (!user) {
    throw new NotFoundException(`User not found (ID: ${id})`);
  }

  return user;
}

InternalServerErrorException (500)

Used when internal processing logic errors occur (including external API calls).
class InternalServerErrorException extends SoException
Usage Example:
@api()
async processPayment(orderId: number) {
  try {
    const result = await externalPaymentAPI.charge(orderId);
    return result;
  } catch (err) {
    throw new InternalServerErrorException(
      "Error occurred during payment processing",
      { orderId, originalError: err }
    );
  }
}

ServiceUnavailableException (503)

Used when processing is impossible in the current state.
class ServiceUnavailableException extends SoException
Usage Example:
@api()
async getRecommendations() {
  if (!this.mlServiceAvailable) {
    throw new ServiceUnavailableException(
      "Recommendation service is temporarily unavailable"
    );
  }

  return this.fetchRecommendations();
}

TargetNotFoundException (520)

Used when there is no target for the operation.
class TargetNotFoundException extends SoException
Usage Example:
@api()
async deleteUserPosts(userId: number) {
  const posts = await this.findByUserId(userId);

  if (posts.length === 0) {
    throw new TargetNotFoundException(
      `No posts to delete (User ID: ${userId})`
    );
  }

  await this.deleteMany(posts.map(p => p.id));
}

AlreadyProcessedException (541)

Used when attempting duplicate processing of an already processed request.
class AlreadyProcessedException extends SoException
Usage Example:
@api()
async processOrder(orderId: number) {
  const order = await this.findById(orderId);

  if (order.status === "processed") {
    throw new AlreadyProcessedException(
      `Order has already been processed (ID: ${orderId})`,
      { processedAt: order.processedAt }
    );
  }

  // Order processing logic
}

DuplicateRowException (542)

Used when duplicate requests are made where duplicates are not allowed.
class DuplicateRowException extends SoException
Usage Example:
@api()
async createUser(email: string, username: string) {
  const existingUser = await this.findByEmail(email);

  if (existingUser) {
    throw new DuplicateRowException(
      "Email already registered",
      { field: "email", value: email }
    );
  }

  // User creation logic
}

Exception Handling Flow

Sonamu automatically processes thrown exceptions and converts them into appropriate HTTP responses:
// Exception thrown in API method
@api()
async createPost(title: string) {
  if (!title) {
    throw new BadRequestException("Title is required");
  }
  // ...
}

// Client receives:
// HTTP 400 Bad Request
// {
//   "statusCode": 400,
//   "message": "Title is required"
// }
When payload is present:
throw new BadRequestException("Invalid data", {
  errors: [
    { field: "email", message: "Email format is invalid" },
    { field: "age", message: "Age must be greater than 0" }
  ]
});

// Client response:
// HTTP 400 Bad Request
// {
//   "statusCode": 400,
//   "message": "Invalid data",
//   "payload": {
//     "errors": [
//       { "field": "email", "message": "Email format is invalid" },
//       { "field": "age", "message": "Age must be greater than 0" }
//     ]
//   }
// }

Exceptions vs Guards

For simple authentication checks, it’s better to use Guards:
// Using Guards (recommended)
@api({ guards: ["user"] })
async getMyData(ctx: Context) {
  // ctx.user is guaranteed
  return this.findById(ctx.user!.id);
}

// Manual check (for more complex logic)
@api()
async updateProfile(ctx: Context, data: ProfileData) {
  if (!ctx.user) {
    throw new UnauthorizedException("Login required");
  }

  if (ctx.user.id !== data.userId) {
    throw new UnauthorizedException("You can only modify your own profile");
  }

  // Profile update logic
}