* This module provides functionality for handling and guarding against exceptions in an API context.
ApiInternalServerErrorException,
ApiUnauthorizedException,
ApiUnprocessableContentException,
} from "@/lib/api-exceptions"
import type { Logger } from "@/lib/logger"
import { Type } from "@sinclair/typebox"
import { Value } from "@sinclair/typebox/value"
import type { StatusMap } from "elysia"
import type { ElysiaCookie } from "elysia/cookies"
import type { HTTPHeaders } from "elysia/types"
/** Represents an error during JSON parsing */
type SafeJsonParseError = [Error, undefined]
/** Represents a successful JSON parse */
type SafeJsonParseSuccess = [undefined, Error]
/** Represents the result of a safe JSON parse operation */
type SafeJsonParseResult = SafeJsonParseError | SafeJsonParseSuccess
* Safely parses a JSON string
* @param obj - The string to parse
* @returns A tuple containing either an Error and undefined, or undefined and the parsed object
function safeParseJson(obj: string): SafeJsonParseResult {
const parsed = JSON.parse(obj)
return [undefined, parsed]
return [error as Error, undefined]
/** Schema for validation errors */
export const ValidationErrorSchema = Type.Object({
/** Possible error codes */
| "INTERNAL_SERVER_ERROR"
| "INVALID_COOKIE_SIGNATURE"
/** State to be set in response */
status?: number | keyof StatusMap
cookie?: Record<string, ElysiaCookie>
/** Result of an error specifier */
type ErrorSpecifierResult<T extends ApiException> =
/** Function to specify how to handle a specific type of ApiException */
type ErrorSpecifier<T extends ApiException> = (exception: ApiException) => ErrorSpecifierResult<T>
/** Options for the ExceptionGuard */
interface ExceptionGuardOptions {
* Builder class for creating an exception guard
export class ExceptionGuardBuilder {
private logger: Logger = console
private errorSpecifiers: ErrorSpecifier<ApiException>[] = []
private options: ExceptionGuardOptions = {}
* Sets the logger for the exception guard
* @param logger - The logger to use
withLogger(logger: Logger) {
* Sets the options for the exception guard
* @param options - The options to set
withOptions(options: ExceptionGuardOptions) {
* Adds an error specifier to the exception guard
* @param specifier - The error specifier to add
withErrorSpecifier<T extends ApiException>(specifier: ErrorSpecifier<T>) {
this.errorSpecifiers.push(specifier)
* Builds and returns the exception guard function
return (code: ErrorCode, set: SetState, error: Error, request: Request) => {
if (code === "VALIDATION") {
let on: string | undefined
let validationErrors: Record<string, string> | undefined
const [_err, parsedMessage] = safeParseJson(error.message)
if (Value.Check(ValidationErrorSchema, parsedMessage)) {
for (const validationError of parsedMessage.errors) {
validationErrors[validationError.path] = validationError.message
// biome-ignore lint/style/noParameterAssign: intentional
error = new ApiUnprocessableContentException("Unprocessable Content", on, validationErrors)
} else if (code === "NOT_FOUND" && !(error instanceof ApiNotFoundException)) {
// biome-ignore lint/style/noParameterAssign: intentional
error = new ApiNotFoundException("Not Found")
} else if (code === "UNAUTHORIZED" && !(error instanceof ApiUnauthorizedException)) {
// biome-ignore lint/style/noParameterAssign: intentional
error = new ApiUnauthorizedException()
const apiError = error instanceof ApiException ? error : new ApiInternalServerErrorException()
const errorLogContext = {
pathname: new URL(request.url).pathname,
if (error !== apiError) {
this.logger.error(error, errorLogContext)
set.status = apiError.status
for (const specifier of this.errorSpecifiers) {
const { match, result } = specifier(apiError)
set.status = result.status
if (this.options.logAllErrors) {
this.logger.error(result, errorLogContext)
if (error === apiError && this.options.logAllErrors) {
this.logger.error(error, errorLogContext)
message: apiError.status === 500 ? "Internal server error" : apiError.message,
* Creates a new ExceptionGuardBuilder with a default error specifier for ApiUnprocessableContentException
* @returns An ExceptionGuardBuilder instance
export function createExceptionGuard() {
return new ExceptionGuardBuilder().withErrorSpecifier<ApiUnprocessableContentException>((apiError) => {
if (apiError instanceof ApiUnprocessableContentException) {
message: apiError.message,