style: auto-format codebase with Prettier

Applied formatting to 1487+ files using pnpm format:write
  - TypeScript/JavaScript files
  - Svelte components
  - Astro pages
  - JSON configs
  - Markdown docs

  13 files still need manual review (Astro JSX comments)
This commit is contained in:
Wuesteon 2025-11-27 18:33:16 +01:00
parent 0241f5554c
commit d36b321d9d
3952 changed files with 661498 additions and 739751 deletions

View file

@ -1,26 +1,22 @@
import {
ErrorCode,
ERROR_CODE_TO_HTTP_STATUS,
ERROR_CODE_RETRYABLE,
} from '../types/error-codes';
import { ErrorCode, ERROR_CODE_TO_HTTP_STATUS, ERROR_CODE_RETRYABLE } from '../types/error-codes';
/**
* Additional context that can be attached to errors.
*/
export interface ErrorContext {
[key: string]: unknown;
[key: string]: unknown;
}
/**
* Options for creating an AppError.
*/
export interface AppErrorOptions {
code: ErrorCode;
message: string;
cause?: Error | AppError;
context?: ErrorContext;
httpStatus?: number;
retryable?: boolean;
code: ErrorCode;
message: string;
cause?: Error | AppError;
context?: ErrorContext;
httpStatus?: number;
retryable?: boolean;
}
/**
@ -50,130 +46,126 @@ export interface AppErrorOptions {
* ```
*/
export class AppError extends Error {
/** Standardized error code */
readonly code: ErrorCode;
/** Standardized error code */
readonly code: ErrorCode;
/** HTTP status code for API responses */
readonly httpStatus: number;
/** HTTP status code for API responses */
readonly httpStatus: number;
/** Whether the operation can be retried */
readonly retryable: boolean;
/** Whether the operation can be retried */
readonly retryable: boolean;
/** Original error that caused this error (for wrapping) */
readonly cause?: Error | AppError;
/** Original error that caused this error (for wrapping) */
readonly cause?: Error | AppError;
/** Additional context information */
readonly context: ErrorContext;
/** Additional context information */
readonly context: ErrorContext;
/** Timestamp when error was created */
readonly timestamp: string;
/** Timestamp when error was created */
readonly timestamp: string;
constructor(options: AppErrorOptions) {
super(options.message);
this.name = 'AppError';
this.code = options.code;
this.cause = options.cause;
this.context = options.context ?? {};
this.timestamp = new Date().toISOString();
constructor(options: AppErrorOptions) {
super(options.message);
this.name = 'AppError';
this.code = options.code;
this.cause = options.cause;
this.context = options.context ?? {};
this.timestamp = new Date().toISOString();
// Use provided values or defaults from mappings
this.httpStatus =
options.httpStatus ?? ERROR_CODE_TO_HTTP_STATUS[options.code];
this.retryable = options.retryable ?? ERROR_CODE_RETRYABLE[options.code];
// Use provided values or defaults from mappings
this.httpStatus = options.httpStatus ?? ERROR_CODE_TO_HTTP_STATUS[options.code];
this.retryable = options.retryable ?? ERROR_CODE_RETRYABLE[options.code];
// Capture stack trace
Error.captureStackTrace(this, this.constructor);
}
// Capture stack trace
Error.captureStackTrace(this, this.constructor);
}
/**
* Create a wrapped error with additional context.
* Similar to Go's `fmt.Errorf("context: %w", err)`.
*
* @param contextMessage - Description of the operation that failed
* @param additionalContext - Extra context data to include
* @returns A new AppError with the original as its cause
*
* @example
* ```typescript
* const wrapped = originalError.wrap('fetching user data');
* // Message: "fetching user data: original message"
* ```
*/
wrap(contextMessage: string, additionalContext?: ErrorContext): AppError {
return new AppError({
code: this.code,
message: `${contextMessage}: ${this.message}`,
cause: this,
context: { ...this.context, ...additionalContext },
httpStatus: this.httpStatus,
retryable: this.retryable,
});
}
/**
* Create a wrapped error with additional context.
* Similar to Go's `fmt.Errorf("context: %w", err)`.
*
* @param contextMessage - Description of the operation that failed
* @param additionalContext - Extra context data to include
* @returns A new AppError with the original as its cause
*
* @example
* ```typescript
* const wrapped = originalError.wrap('fetching user data');
* // Message: "fetching user data: original message"
* ```
*/
wrap(contextMessage: string, additionalContext?: ErrorContext): AppError {
return new AppError({
code: this.code,
message: `${contextMessage}: ${this.message}`,
cause: this,
context: { ...this.context, ...additionalContext },
httpStatus: this.httpStatus,
retryable: this.retryable,
});
}
/**
* Get the root cause of the error chain.
* Traverses the cause chain to find the original error.
*/
rootCause(): Error {
let current: Error = this;
while (current instanceof AppError && current.cause) {
current = current.cause;
}
return current;
}
/**
* Get the root cause of the error chain.
* Traverses the cause chain to find the original error.
*/
rootCause(): Error {
let current: Error = this;
while (current instanceof AppError && current.cause) {
current = current.cause;
}
return current;
}
/**
* Check if this error or any in the chain has the given code.
* Similar to Go's `errors.Is()`.
*
* @param code - The error code to check for
* @returns true if this error or any cause has the given code
*
* @example
* ```typescript
* if (error.hasCode(ErrorCode.INSUFFICIENT_CREDITS)) {
* // Show upgrade prompt
* }
* ```
*/
hasCode(code: ErrorCode): boolean {
let current: Error | undefined = this;
while (current) {
if (current instanceof AppError && current.code === code) {
return true;
}
current = current instanceof AppError ? current.cause : undefined;
}
return false;
}
/**
* Check if this error or any in the chain has the given code.
* Similar to Go's `errors.Is()`.
*
* @param code - The error code to check for
* @returns true if this error or any cause has the given code
*
* @example
* ```typescript
* if (error.hasCode(ErrorCode.INSUFFICIENT_CREDITS)) {
* // Show upgrade prompt
* }
* ```
*/
hasCode(code: ErrorCode): boolean {
let current: Error | undefined = this;
while (current) {
if (current instanceof AppError && current.code === code) {
return true;
}
current = current instanceof AppError ? current.cause : undefined;
}
return false;
}
/**
* Convert to JSON for API responses.
* Excludes stack traces and internal details.
*/
toJSON(): Record<string, unknown> {
return {
code: this.code,
message: this.message,
httpStatus: this.httpStatus,
retryable: this.retryable,
timestamp: this.timestamp,
...(Object.keys(this.context).length > 0 && { details: this.context }),
};
}
/**
* Convert to JSON for API responses.
* Excludes stack traces and internal details.
*/
toJSON(): Record<string, unknown> {
return {
code: this.code,
message: this.message,
httpStatus: this.httpStatus,
retryable: this.retryable,
timestamp: this.timestamp,
...(Object.keys(this.context).length > 0 && { details: this.context }),
};
}
/**
* Convert to full JSON including stack and cause (for logging).
* Use this for server-side logging, not client responses.
*/
toFullJSON(): Record<string, unknown> {
return {
...this.toJSON(),
stack: this.stack,
cause:
this.cause instanceof AppError
? this.cause.toFullJSON()
: this.cause?.message,
};
}
/**
* Convert to full JSON including stack and cause (for logging).
* Use this for server-side logging, not client responses.
*/
toFullJSON(): Record<string, unknown> {
return {
...this.toJSON(),
stack: this.stack,
cause: this.cause instanceof AppError ? this.cause.toFullJSON() : this.cause?.message,
};
}
}

View file

@ -2,11 +2,11 @@ import { ErrorCode } from '../types/error-codes';
import { AppError, type ErrorContext } from './app-error';
type AuthErrorCode =
| ErrorCode.AUTHENTICATION_REQUIRED
| ErrorCode.INVALID_TOKEN
| ErrorCode.TOKEN_EXPIRED
| ErrorCode.PERMISSION_DENIED
| ErrorCode.RESOURCE_NOT_OWNED;
| ErrorCode.AUTHENTICATION_REQUIRED
| ErrorCode.INVALID_TOKEN
| ErrorCode.TOKEN_EXPIRED
| ErrorCode.PERMISSION_DENIED
| ErrorCode.RESOURCE_NOT_OWNED;
/**
* Error for authentication and authorization failures.
@ -25,55 +25,54 @@ type AuthErrorCode =
* ```
*/
export class AuthError extends AppError {
constructor(code: AuthErrorCode, message: string, context?: ErrorContext) {
super({ code, message, context });
this.name = 'AuthError';
}
constructor(code: AuthErrorCode, message: string, context?: ErrorContext) {
super({ code, message, context });
this.name = 'AuthError';
}
/**
* Create an error for missing authentication.
* HTTP 401 Unauthorized
*/
static unauthorized(message = 'Authentication required'): AuthError {
return new AuthError(ErrorCode.AUTHENTICATION_REQUIRED, message);
}
/**
* Create an error for missing authentication.
* HTTP 401 Unauthorized
*/
static unauthorized(message = 'Authentication required'): AuthError {
return new AuthError(ErrorCode.AUTHENTICATION_REQUIRED, message);
}
/**
* Create an error for an invalid token.
* HTTP 401 Unauthorized
*/
static invalidToken(message = 'Invalid or malformed token'): AuthError {
return new AuthError(ErrorCode.INVALID_TOKEN, message);
}
/**
* Create an error for an invalid token.
* HTTP 401 Unauthorized
*/
static invalidToken(message = 'Invalid or malformed token'): AuthError {
return new AuthError(ErrorCode.INVALID_TOKEN, message);
}
/**
* Create an error for an expired token.
* HTTP 401 Unauthorized
*/
static tokenExpired(message = 'Token has expired'): AuthError {
return new AuthError(ErrorCode.TOKEN_EXPIRED, message);
}
/**
* Create an error for an expired token.
* HTTP 401 Unauthorized
*/
static tokenExpired(message = 'Token has expired'): AuthError {
return new AuthError(ErrorCode.TOKEN_EXPIRED, message);
}
/**
* Create an error for insufficient permissions.
* HTTP 403 Forbidden
*/
static forbidden(message = 'Permission denied'): AuthError {
return new AuthError(ErrorCode.PERMISSION_DENIED, message);
}
/**
* Create an error for insufficient permissions.
* HTTP 403 Forbidden
*/
static forbidden(message = 'Permission denied'): AuthError {
return new AuthError(ErrorCode.PERMISSION_DENIED, message);
}
/**
* Create an error when a user tries to access a resource they don't own.
* HTTP 403 Forbidden
*
* @param resourceType - Type of resource (e.g., 'Story', 'Character')
* @param resourceId - ID of the resource
*/
static notOwned(resourceType: string, resourceId: string): AuthError {
return new AuthError(
ErrorCode.RESOURCE_NOT_OWNED,
`${resourceType} does not belong to you`,
{ resourceType, resourceId }
);
}
/**
* Create an error when a user tries to access a resource they don't own.
* HTTP 403 Forbidden
*
* @param resourceType - Type of resource (e.g., 'Story', 'Character')
* @param resourceId - ID of the resource
*/
static notOwned(resourceType: string, resourceId: string): AuthError {
return new AuthError(ErrorCode.RESOURCE_NOT_OWNED, `${resourceType} does not belong to you`, {
resourceType,
resourceId,
});
}
}

View file

@ -12,24 +12,20 @@ import { AppError } from './app-error';
* ```
*/
export class CreditError extends AppError {
/** Credits required for the operation */
readonly requiredCredits: number;
/** Credits required for the operation */
readonly requiredCredits: number;
/** Credits currently available */
readonly availableCredits: number;
/** Credits currently available */
readonly availableCredits: number;
constructor(
requiredCredits: number,
availableCredits: number,
operation?: string
) {
super({
code: ErrorCode.INSUFFICIENT_CREDITS,
message: `Insufficient credits. Required: ${requiredCredits}, Available: ${availableCredits}`,
context: { requiredCredits, availableCredits, operation },
});
this.name = 'CreditError';
this.requiredCredits = requiredCredits;
this.availableCredits = availableCredits;
}
constructor(requiredCredits: number, availableCredits: number, operation?: string) {
super({
code: ErrorCode.INSUFFICIENT_CREDITS,
message: `Insufficient credits. Required: ${requiredCredits}, Available: ${availableCredits}`,
context: { requiredCredits, availableCredits, operation },
});
this.name = 'CreditError';
this.requiredCredits = requiredCredits;
this.availableCredits = availableCredits;
}
}

View file

@ -17,38 +17,28 @@ type DatabaseErrorCode = ErrorCode.DATABASE_ERROR | ErrorCode.CONSTRAINT_VIOLATI
* ```
*/
export class DatabaseError extends AppError {
constructor(
code: DatabaseErrorCode,
message: string,
cause?: Error,
context?: ErrorContext
) {
super({ code, message, cause, context });
this.name = 'DatabaseError';
}
constructor(code: DatabaseErrorCode, message: string, cause?: Error, context?: ErrorContext) {
super({ code, message, cause, context });
this.name = 'DatabaseError';
}
/**
* Create a constraint violation error (e.g., unique constraint).
*
* @param field - The field that violated the constraint
* @param message - Description of the violation
*/
static constraintViolation(field: string, message: string): DatabaseError {
return new DatabaseError(
ErrorCode.CONSTRAINT_VIOLATION,
message,
undefined,
{ field }
);
}
/**
* Create a constraint violation error (e.g., unique constraint).
*
* @param field - The field that violated the constraint
* @param message - Description of the violation
*/
static constraintViolation(field: string, message: string): DatabaseError {
return new DatabaseError(ErrorCode.CONSTRAINT_VIOLATION, message, undefined, { field });
}
/**
* Create a generic database query error.
*
* @param message - Description of what went wrong
* @param cause - Original error if available
*/
static queryFailed(message: string, cause?: Error): DatabaseError {
return new DatabaseError(ErrorCode.DATABASE_ERROR, message, cause);
}
/**
* Create a generic database query error.
*
* @param message - Description of what went wrong
* @param cause - Original error if available
*/
static queryFailed(message: string, cause?: Error): DatabaseError {
return new DatabaseError(ErrorCode.DATABASE_ERROR, message, cause);
}
}

View file

@ -1,10 +1,7 @@
import { ErrorCode } from '../types/error-codes';
import { AppError, type ErrorContext } from './app-error';
type NetworkErrorCode =
| ErrorCode.NETWORK_ERROR
| ErrorCode.TIMEOUT
| ErrorCode.CONNECTION_REFUSED;
type NetworkErrorCode = ErrorCode.NETWORK_ERROR | ErrorCode.TIMEOUT | ErrorCode.CONNECTION_REFUSED;
/**
* Error for network-level failures (timeouts, connection issues, etc.).
@ -23,41 +20,33 @@ type NetworkErrorCode =
* ```
*/
export class NetworkError extends AppError {
constructor(
code: NetworkErrorCode,
message: string,
cause?: Error,
context?: ErrorContext
) {
super({ code, message, cause, context });
this.name = 'NetworkError';
}
constructor(code: NetworkErrorCode, message: string, cause?: Error, context?: ErrorContext) {
super({ code, message, cause, context });
this.name = 'NetworkError';
}
/**
* Create a timeout error.
*
* @param operation - Description of the operation that timed out
*/
static timeout(operation: string): NetworkError {
return new NetworkError(
ErrorCode.TIMEOUT,
`Operation timed out: ${operation}`,
undefined,
{ operation }
);
}
/**
* Create a timeout error.
*
* @param operation - Description of the operation that timed out
*/
static timeout(operation: string): NetworkError {
return new NetworkError(ErrorCode.TIMEOUT, `Operation timed out: ${operation}`, undefined, {
operation,
});
}
/**
* Create a connection refused error.
*
* @param service - Name of the service that refused connection
*/
static connectionRefused(service: string): NetworkError {
return new NetworkError(
ErrorCode.CONNECTION_REFUSED,
`Connection refused: ${service}`,
undefined,
{ service }
);
}
/**
* Create a connection refused error.
*
* @param service - Name of the service that refused connection
*/
static connectionRefused(service: string): NetworkError {
return new NetworkError(
ErrorCode.CONNECTION_REFUSED,
`Connection refused: ${service}`,
undefined,
{ service }
);
}
}

View file

@ -16,30 +16,26 @@ import { AppError, type ErrorContext } from './app-error';
* ```
*/
export class NotFoundError extends AppError {
constructor(
resourceType: string,
identifier: string,
context?: ErrorContext
) {
super({
code: ErrorCode.RESOURCE_NOT_FOUND,
message: `${resourceType} not found: ${identifier}`,
context: { resourceType, identifier, ...context },
});
this.name = 'NotFoundError';
}
constructor(resourceType: string, identifier: string, context?: ErrorContext) {
super({
code: ErrorCode.RESOURCE_NOT_FOUND,
message: `${resourceType} not found: ${identifier}`,
context: { resourceType, identifier, ...context },
});
this.name = 'NotFoundError';
}
/**
* Create a not found error for a user.
*/
static user(userId: string): NotFoundError {
return new NotFoundError('User', userId);
}
/**
* Create a not found error for a user.
*/
static user(userId: string): NotFoundError {
return new NotFoundError('User', userId);
}
/**
* Create a not found error for any resource type.
*/
static resource(resourceType: string, identifier: string): NotFoundError {
return new NotFoundError(resourceType, identifier);
}
/**
* Create a not found error for any resource type.
*/
static resource(resourceType: string, identifier: string): NotFoundError {
return new NotFoundError(resourceType, identifier);
}
}

View file

@ -16,16 +16,16 @@ import { AppError } from './app-error';
* ```
*/
export class RateLimitError extends AppError {
/** Seconds to wait before retrying (if known) */
readonly retryAfter?: number;
/** Seconds to wait before retrying (if known) */
readonly retryAfter?: number;
constructor(message = 'Rate limit exceeded', retryAfter?: number) {
super({
code: ErrorCode.RATE_LIMIT_EXCEEDED,
message,
context: retryAfter ? { retryAfter } : {},
});
this.name = 'RateLimitError';
this.retryAfter = retryAfter;
}
constructor(message = 'Rate limit exceeded', retryAfter?: number) {
super({
code: ErrorCode.RATE_LIMIT_EXCEEDED,
message,
context: retryAfter ? { retryAfter } : {},
});
this.name = 'RateLimitError';
this.retryAfter = retryAfter;
}
}

View file

@ -2,10 +2,10 @@ import { ErrorCode } from '../types/error-codes';
import { AppError, type ErrorContext } from './app-error';
type ServiceErrorCode =
| ErrorCode.INTERNAL_ERROR
| ErrorCode.SERVICE_UNAVAILABLE
| ErrorCode.GENERATION_FAILED
| ErrorCode.EXTERNAL_SERVICE_ERROR;
| ErrorCode.INTERNAL_ERROR
| ErrorCode.SERVICE_UNAVAILABLE
| ErrorCode.GENERATION_FAILED
| ErrorCode.EXTERNAL_SERVICE_ERROR;
/**
* Error for service-level failures (internal errors, external API failures, etc.).
@ -27,77 +27,64 @@ type ServiceErrorCode =
* ```
*/
export class ServiceError extends AppError {
constructor(
code: ServiceErrorCode,
message: string,
cause?: Error,
context?: ErrorContext
) {
super({ code, message, cause, context });
this.name = 'ServiceError';
}
constructor(code: ServiceErrorCode, message: string, cause?: Error, context?: ErrorContext) {
super({ code, message, cause, context });
this.name = 'ServiceError';
}
/**
* Create an error for AI/content generation failures.
*
* @param service - Name of the service (e.g., 'OpenAI', 'Azure OpenAI')
* @param reason - Why the generation failed
* @param cause - Original error if available
*/
static generationFailed(
service: string,
reason: string,
cause?: Error
): ServiceError {
return new ServiceError(
ErrorCode.GENERATION_FAILED,
`${service} generation failed: ${reason}`,
cause,
{ service }
);
}
/**
* Create an error for AI/content generation failures.
*
* @param service - Name of the service (e.g., 'OpenAI', 'Azure OpenAI')
* @param reason - Why the generation failed
* @param cause - Original error if available
*/
static generationFailed(service: string, reason: string, cause?: Error): ServiceError {
return new ServiceError(
ErrorCode.GENERATION_FAILED,
`${service} generation failed: ${reason}`,
cause,
{ service }
);
}
/**
* Create an error for a service that is temporarily unavailable.
*
* @param service - Name of the unavailable service
*/
static unavailable(service: string): ServiceError {
return new ServiceError(
ErrorCode.SERVICE_UNAVAILABLE,
`${service} is temporarily unavailable`,
undefined,
{ service }
);
}
/**
* Create an error for a service that is temporarily unavailable.
*
* @param service - Name of the unavailable service
*/
static unavailable(service: string): ServiceError {
return new ServiceError(
ErrorCode.SERVICE_UNAVAILABLE,
`${service} is temporarily unavailable`,
undefined,
{ service }
);
}
/**
* Create an error for external API failures.
*
* @param service - Name of the external service
* @param message - Error message or description
* @param cause - Original error if available
*/
static externalError(
service: string,
message: string,
cause?: Error
): ServiceError {
return new ServiceError(
ErrorCode.EXTERNAL_SERVICE_ERROR,
`${service} error: ${message}`,
cause,
{ service }
);
}
/**
* Create an error for external API failures.
*
* @param service - Name of the external service
* @param message - Error message or description
* @param cause - Original error if available
*/
static externalError(service: string, message: string, cause?: Error): ServiceError {
return new ServiceError(
ErrorCode.EXTERNAL_SERVICE_ERROR,
`${service} error: ${message}`,
cause,
{ service }
);
}
/**
* Create an internal server error.
*
* @param message - Description of what went wrong
* @param cause - Original error if available
*/
static internal(message: string, cause?: Error): ServiceError {
return new ServiceError(ErrorCode.INTERNAL_ERROR, message, cause);
}
/**
* Create an internal server error.
*
* @param message - Description of what went wrong
* @param cause - Original error if available
*/
static internal(message: string, cause?: Error): ServiceError {
return new ServiceError(ErrorCode.INTERNAL_ERROR, message, cause);
}
}

View file

@ -16,44 +16,44 @@ import { AppError, type ErrorContext } from './app-error';
* ```
*/
export class ValidationError extends AppError {
constructor(message: string, context?: ErrorContext) {
super({
code: ErrorCode.VALIDATION_FAILED,
message,
context,
});
this.name = 'ValidationError';
}
constructor(message: string, context?: ErrorContext) {
super({
code: ErrorCode.VALIDATION_FAILED,
message,
context,
});
this.name = 'ValidationError';
}
/**
* Create a validation error for an invalid field value.
*
* @param field - The field name that failed validation
* @param reason - Why the validation failed
*/
static invalidInput(field: string, reason: string): ValidationError {
return new ValidationError(`Invalid ${field}: ${reason}`, { field, reason });
}
/**
* Create a validation error for an invalid field value.
*
* @param field - The field name that failed validation
* @param reason - Why the validation failed
*/
static invalidInput(field: string, reason: string): ValidationError {
return new ValidationError(`Invalid ${field}: ${reason}`, { field, reason });
}
/**
* Create a validation error for a missing required field.
*
* @param field - The field name that is missing
*/
static missingField(field: string): ValidationError {
return new ValidationError(`Missing required field: ${field}`, { field });
}
/**
* Create a validation error for a missing required field.
*
* @param field - The field name that is missing
*/
static missingField(field: string): ValidationError {
return new ValidationError(`Missing required field: ${field}`, { field });
}
/**
* Create a validation error for an invalid format.
*
* @param field - The field name with invalid format
* @param expectedFormat - Description of the expected format
*/
static invalidFormat(field: string, expectedFormat: string): ValidationError {
return new ValidationError(
`Invalid format for ${field}: expected ${expectedFormat}`,
{ field, expectedFormat }
);
}
/**
* Create a validation error for an invalid format.
*
* @param field - The field name with invalid format
* @param expectedFormat - Description of the expected format
*/
static invalidFormat(field: string, expectedFormat: string): ValidationError {
return new ValidationError(`Invalid format for ${field}: expected ${expectedFormat}`, {
field,
expectedFormat,
});
}
}

View file

@ -21,63 +21,63 @@ import { ErrorCode } from '../types/error-codes';
* ```
*/
export function isAppError(error: unknown): error is AppError {
return error instanceof AppError;
return error instanceof AppError;
}
/**
* Check if error is a ValidationError.
*/
export function isValidationError(error: unknown): error is ValidationError {
return error instanceof ValidationError;
return error instanceof ValidationError;
}
/**
* Check if error is an AuthError.
*/
export function isAuthError(error: unknown): error is AuthError {
return error instanceof AuthError;
return error instanceof AuthError;
}
/**
* Check if error is a NotFoundError.
*/
export function isNotFoundError(error: unknown): error is NotFoundError {
return error instanceof NotFoundError;
return error instanceof NotFoundError;
}
/**
* Check if error is a CreditError.
*/
export function isCreditError(error: unknown): error is CreditError {
return error instanceof CreditError;
return error instanceof CreditError;
}
/**
* Check if error is a ServiceError.
*/
export function isServiceError(error: unknown): error is ServiceError {
return error instanceof ServiceError;
return error instanceof ServiceError;
}
/**
* Check if error is a RateLimitError.
*/
export function isRateLimitError(error: unknown): error is RateLimitError {
return error instanceof RateLimitError;
return error instanceof RateLimitError;
}
/**
* Check if error is a NetworkError.
*/
export function isNetworkError(error: unknown): error is NetworkError {
return error instanceof NetworkError;
return error instanceof NetworkError;
}
/**
* Check if error is a DatabaseError.
*/
export function isDatabaseError(error: unknown): error is DatabaseError {
return error instanceof DatabaseError;
return error instanceof DatabaseError;
}
/**
@ -92,10 +92,10 @@ export function isDatabaseError(error: unknown): error is DatabaseError {
* ```
*/
export function hasErrorCode(error: unknown, code: ErrorCode): boolean {
if (!isAppError(error)) {
return false;
}
return error.hasCode(code);
if (!isAppError(error)) {
return false;
}
return error.hasCode(code);
}
/**
@ -111,17 +111,17 @@ export function hasErrorCode(error: unknown, code: ErrorCode): boolean {
* ```
*/
export function findError<T extends AppError>(
error: unknown,
predicate: (e: AppError) => e is T
error: unknown,
predicate: (e: AppError) => e is T
): T | undefined {
let current: unknown = error;
while (current) {
if (isAppError(current) && predicate(current)) {
return current;
}
current = isAppError(current) ? current.cause : undefined;
}
return undefined;
let current: unknown = error;
while (current) {
if (isAppError(current) && predicate(current)) {
return current;
}
current = isAppError(current) ? current.cause : undefined;
}
return undefined;
}
/**
@ -129,10 +129,10 @@ export function findError<T extends AppError>(
* Works with both AppError and standard Error.
*/
export function isRetryable(error: unknown): boolean {
if (isAppError(error)) {
return error.retryable;
}
return false;
if (isAppError(error)) {
return error.retryable;
}
return false;
}
/**
@ -140,10 +140,10 @@ export function isRetryable(error: unknown): boolean {
* Returns 500 for non-AppError errors.
*/
export function getHttpStatus(error: unknown): number {
if (isAppError(error)) {
return error.httpStatus;
}
return 500;
if (isAppError(error)) {
return error.httpStatus;
}
return 500;
}
/**
@ -151,8 +151,8 @@ export function getHttpStatus(error: unknown): number {
* Returns UNKNOWN_ERROR for non-AppError errors.
*/
export function getErrorCode(error: unknown): ErrorCode {
if (isAppError(error)) {
return error.code;
}
return ErrorCode.UNKNOWN_ERROR;
if (isAppError(error)) {
return error.code;
}
return ErrorCode.UNKNOWN_ERROR;
}

View file

@ -43,39 +43,31 @@
*/
// Types
export {
ErrorCode,
ERROR_CODE_TO_HTTP_STATUS,
ERROR_CODE_RETRYABLE,
} from './types/error-codes';
export { ErrorCode, ERROR_CODE_TO_HTTP_STATUS, ERROR_CODE_RETRYABLE } from './types/error-codes';
export {
type Result,
type AsyncResult,
ok,
err,
isOk,
isErr,
unwrap,
unwrapOr,
unwrapOrElse,
map,
mapErr,
andThen,
match,
tryCatch,
tryCatchAsync,
combine,
fromNullable,
toNullable,
type Result,
type AsyncResult,
ok,
err,
isOk,
isErr,
unwrap,
unwrapOr,
unwrapOrElse,
map,
mapErr,
andThen,
match,
tryCatch,
tryCatchAsync,
combine,
fromNullable,
toNullable,
} from './types/result';
// Errors
export {
AppError,
type ErrorContext,
type AppErrorOptions,
} from './errors/app-error';
export { AppError, type ErrorContext, type AppErrorOptions } from './errors/app-error';
export { ValidationError } from './errors/validation-error';
export { AuthError } from './errors/auth-error';
@ -88,20 +80,20 @@ export { DatabaseError } from './errors/database-error';
// Guards
export {
isAppError,
isValidationError,
isAuthError,
isNotFoundError,
isCreditError,
isServiceError,
isRateLimitError,
isNetworkError,
isDatabaseError,
hasErrorCode,
findError,
isRetryable,
getHttpStatus,
getErrorCode,
isAppError,
isValidationError,
isAuthError,
isNotFoundError,
isCreditError,
isServiceError,
isRateLimitError,
isNetworkError,
isDatabaseError,
hasErrorCode,
findError,
isRetryable,
getHttpStatus,
getErrorCode,
} from './guards/type-guards';
// Utils

View file

@ -1,10 +1,10 @@
import {
type ExceptionFilter,
Catch,
type ArgumentsHost,
HttpException,
HttpStatus,
Logger,
type ExceptionFilter,
Catch,
type ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import type { Request, Response } from 'express';
import { AppError } from '../errors/app-error';
@ -15,13 +15,13 @@ import { ErrorCode } from '../types/error-codes';
* Standard error response format returned by all backends.
*/
export interface ErrorResponseBody {
statusCode: number;
error: string;
message: string;
retryable: boolean;
timestamp: string;
path: string;
details?: Record<string, unknown>;
statusCode: number;
error: string;
message: string;
retryable: boolean;
timestamp: string;
path: string;
details?: Record<string, unknown>;
}
/**
@ -47,213 +47,198 @@ export interface ErrorResponseBody {
*/
@Catch()
export class AppExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(AppExceptionFilter.name);
private readonly logger = new Logger(AppExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const errorResponse = this.buildErrorResponse(exception, request);
const errorResponse = this.buildErrorResponse(exception, request);
this.logError(exception, request, errorResponse);
this.logError(exception, request, errorResponse);
response.status(errorResponse.statusCode).json(errorResponse);
}
response.status(errorResponse.statusCode).json(errorResponse);
}
/**
* Build the error response body based on the exception type.
*/
private buildErrorResponse(
exception: unknown,
request: Request
): ErrorResponseBody {
// Handle AppError and subclasses
if (isAppError(exception)) {
return this.buildAppErrorResponse(exception, request);
}
/**
* Build the error response body based on the exception type.
*/
private buildErrorResponse(exception: unknown, request: Request): ErrorResponseBody {
// Handle AppError and subclasses
if (isAppError(exception)) {
return this.buildAppErrorResponse(exception, request);
}
// Handle NestJS HttpException
if (exception instanceof HttpException) {
return this.buildHttpExceptionResponse(exception, request);
}
// Handle NestJS HttpException
if (exception instanceof HttpException) {
return this.buildHttpExceptionResponse(exception, request);
}
// Handle standard Error
if (exception instanceof Error) {
return this.buildStandardErrorResponse(exception, request);
}
// Handle standard Error
if (exception instanceof Error) {
return this.buildStandardErrorResponse(exception, request);
}
// Handle unknown errors
return this.buildUnknownErrorResponse(request);
}
// Handle unknown errors
return this.buildUnknownErrorResponse(request);
}
/**
* Build response for AppError and subclasses.
*/
private buildAppErrorResponse(
exception: AppError,
request: Request
): ErrorResponseBody {
const baseResponse: ErrorResponseBody = {
statusCode: exception.httpStatus,
error: exception.code,
message: exception.message,
retryable: exception.retryable,
timestamp: exception.timestamp,
path: request.url,
};
/**
* Build response for AppError and subclasses.
*/
private buildAppErrorResponse(exception: AppError, request: Request): ErrorResponseBody {
const baseResponse: ErrorResponseBody = {
statusCode: exception.httpStatus,
error: exception.code,
message: exception.message,
retryable: exception.retryable,
timestamp: exception.timestamp,
path: request.url,
};
// Add credit-specific fields for CreditError
if (isCreditError(exception)) {
baseResponse.details = {
requiredCredits: exception.requiredCredits,
availableCredits: exception.availableCredits,
...exception.context,
};
}
// Add retry-after for RateLimitError
else if (isRateLimitError(exception) && exception.retryAfter) {
baseResponse.details = {
retryAfter: exception.retryAfter,
...exception.context,
};
}
// Add other context if present
else if (Object.keys(exception.context).length > 0) {
baseResponse.details = exception.context;
}
// Add credit-specific fields for CreditError
if (isCreditError(exception)) {
baseResponse.details = {
requiredCredits: exception.requiredCredits,
availableCredits: exception.availableCredits,
...exception.context,
};
}
// Add retry-after for RateLimitError
else if (isRateLimitError(exception) && exception.retryAfter) {
baseResponse.details = {
retryAfter: exception.retryAfter,
...exception.context,
};
}
// Add other context if present
else if (Object.keys(exception.context).length > 0) {
baseResponse.details = exception.context;
}
return baseResponse;
}
return baseResponse;
}
/**
* Build response for NestJS HttpException.
*/
private buildHttpExceptionResponse(
exception: HttpException,
request: Request
): ErrorResponseBody {
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
/**
* Build response for NestJS HttpException.
*/
private buildHttpExceptionResponse(
exception: HttpException,
request: Request
): ErrorResponseBody {
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
let message: string;
let details: Record<string, unknown> | undefined;
let message: string;
let details: Record<string, unknown> | undefined;
if (typeof exceptionResponse === 'object') {
const responseObj = exceptionResponse as Record<string, unknown>;
message =
typeof responseObj.message === 'string'
? responseObj.message
: Array.isArray(responseObj.message)
? (responseObj.message as string[]).join(', ')
: exception.message;
if (typeof exceptionResponse === 'object') {
const responseObj = exceptionResponse as Record<string, unknown>;
message =
typeof responseObj.message === 'string'
? responseObj.message
: Array.isArray(responseObj.message)
? (responseObj.message as string[]).join(', ')
: exception.message;
// Extract any additional details
const { message: _, error: __, statusCode: ___, ...rest } = responseObj;
if (Object.keys(rest).length > 0) {
details = rest;
}
} else {
message = String(exceptionResponse);
}
// Extract any additional details
const { message: _, error: __, statusCode: ___, ...rest } = responseObj;
if (Object.keys(rest).length > 0) {
details = rest;
}
} else {
message = String(exceptionResponse);
}
return {
statusCode: status,
error: this.httpStatusToErrorCode(status),
message,
retryable: status >= 500,
timestamp: new Date().toISOString(),
path: request.url,
...(details && { details }),
};
}
return {
statusCode: status,
error: this.httpStatusToErrorCode(status),
message,
retryable: status >= 500,
timestamp: new Date().toISOString(),
path: request.url,
...(details && { details }),
};
}
/**
* Build response for standard JavaScript Error.
*/
private buildStandardErrorResponse(
exception: Error,
request: Request
): ErrorResponseBody {
const isProduction = process.env.NODE_ENV === 'production';
/**
* Build response for standard JavaScript Error.
*/
private buildStandardErrorResponse(exception: Error, request: Request): ErrorResponseBody {
const isProduction = process.env.NODE_ENV === 'production';
return {
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
error: ErrorCode.INTERNAL_ERROR,
message: isProduction
? 'An unexpected error occurred'
: exception.message,
retryable: true,
timestamp: new Date().toISOString(),
path: request.url,
};
}
return {
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
error: ErrorCode.INTERNAL_ERROR,
message: isProduction ? 'An unexpected error occurred' : exception.message,
retryable: true,
timestamp: new Date().toISOString(),
path: request.url,
};
}
/**
* Build response for unknown error types.
*/
private buildUnknownErrorResponse(request: Request): ErrorResponseBody {
return {
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
error: ErrorCode.UNKNOWN_ERROR,
message: 'An unexpected error occurred',
retryable: true,
timestamp: new Date().toISOString(),
path: request.url,
};
}
/**
* Build response for unknown error types.
*/
private buildUnknownErrorResponse(request: Request): ErrorResponseBody {
return {
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
error: ErrorCode.UNKNOWN_ERROR,
message: 'An unexpected error occurred',
retryable: true,
timestamp: new Date().toISOString(),
path: request.url,
};
}
/**
* Map HTTP status code to ErrorCode.
*/
private httpStatusToErrorCode(status: number): string {
const statusToCode: Record<number, string> = {
400: ErrorCode.VALIDATION_FAILED,
401: ErrorCode.AUTHENTICATION_REQUIRED,
402: ErrorCode.PAYMENT_REQUIRED,
403: ErrorCode.PERMISSION_DENIED,
404: ErrorCode.RESOURCE_NOT_FOUND,
409: ErrorCode.CONFLICT,
429: ErrorCode.RATE_LIMIT_EXCEEDED,
500: ErrorCode.INTERNAL_ERROR,
502: ErrorCode.EXTERNAL_SERVICE_ERROR,
503: ErrorCode.SERVICE_UNAVAILABLE,
504: ErrorCode.TIMEOUT,
};
return statusToCode[status] || ErrorCode.UNKNOWN_ERROR;
}
/**
* Map HTTP status code to ErrorCode.
*/
private httpStatusToErrorCode(status: number): string {
const statusToCode: Record<number, string> = {
400: ErrorCode.VALIDATION_FAILED,
401: ErrorCode.AUTHENTICATION_REQUIRED,
402: ErrorCode.PAYMENT_REQUIRED,
403: ErrorCode.PERMISSION_DENIED,
404: ErrorCode.RESOURCE_NOT_FOUND,
409: ErrorCode.CONFLICT,
429: ErrorCode.RATE_LIMIT_EXCEEDED,
500: ErrorCode.INTERNAL_ERROR,
502: ErrorCode.EXTERNAL_SERVICE_ERROR,
503: ErrorCode.SERVICE_UNAVAILABLE,
504: ErrorCode.TIMEOUT,
};
return statusToCode[status] || ErrorCode.UNKNOWN_ERROR;
}
/**
* Log the error with appropriate level based on status code.
*/
private logError(
exception: unknown,
request: Request,
response: ErrorResponseBody
): void {
const logData = {
method: request.method,
url: request.url,
statusCode: response.statusCode,
error: response.error,
message: response.message,
userId: (request as Request & { user?: { sub?: string } }).user?.sub,
};
/**
* Log the error with appropriate level based on status code.
*/
private logError(exception: unknown, request: Request, response: ErrorResponseBody): void {
const logData = {
method: request.method,
url: request.url,
statusCode: response.statusCode,
error: response.error,
message: response.message,
userId: (request as Request & { user?: { sub?: string } }).user?.sub,
};
// Log 5xx errors as errors, others as warnings
if (response.statusCode >= 500) {
this.logger.error(
`[${logData.method}] ${logData.url} - ${logData.statusCode}: ${logData.message}`,
isAppError(exception)
? JSON.stringify(exception.toFullJSON(), null, 2)
: exception instanceof Error
? exception.stack
: undefined
);
} else {
this.logger.warn(
`[${logData.method}] ${logData.url} - ${logData.statusCode}: ${logData.message}`
);
}
}
// Log 5xx errors as errors, others as warnings
if (response.statusCode >= 500) {
this.logger.error(
`[${logData.method}] ${logData.url} - ${logData.statusCode}: ${logData.message}`,
isAppError(exception)
? JSON.stringify(exception.toFullJSON(), null, 2)
: exception instanceof Error
? exception.stack
: undefined
);
} else {
this.logger.warn(
`[${logData.method}] ${logData.url} - ${logData.statusCode}: ${logData.message}`
);
}
}
}

View file

@ -3,160 +3,160 @@
* Follows pattern: CATEGORY_SPECIFIC_ERROR
*/
export enum ErrorCode {
// Validation Errors (400)
VALIDATION_FAILED = 'VALIDATION_FAILED',
INVALID_INPUT = 'INVALID_INPUT',
MISSING_REQUIRED_FIELD = 'MISSING_REQUIRED_FIELD',
INVALID_FORMAT = 'INVALID_FORMAT',
// Validation Errors (400)
VALIDATION_FAILED = 'VALIDATION_FAILED',
INVALID_INPUT = 'INVALID_INPUT',
MISSING_REQUIRED_FIELD = 'MISSING_REQUIRED_FIELD',
INVALID_FORMAT = 'INVALID_FORMAT',
// Authentication Errors (401)
AUTHENTICATION_REQUIRED = 'AUTHENTICATION_REQUIRED',
INVALID_TOKEN = 'INVALID_TOKEN',
TOKEN_EXPIRED = 'TOKEN_EXPIRED',
// Authentication Errors (401)
AUTHENTICATION_REQUIRED = 'AUTHENTICATION_REQUIRED',
INVALID_TOKEN = 'INVALID_TOKEN',
TOKEN_EXPIRED = 'TOKEN_EXPIRED',
// Authorization Errors (403)
PERMISSION_DENIED = 'PERMISSION_DENIED',
RESOURCE_NOT_OWNED = 'RESOURCE_NOT_OWNED',
// Authorization Errors (403)
PERMISSION_DENIED = 'PERMISSION_DENIED',
RESOURCE_NOT_OWNED = 'RESOURCE_NOT_OWNED',
// Not Found Errors (404)
RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND',
USER_NOT_FOUND = 'USER_NOT_FOUND',
// Not Found Errors (404)
RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND',
USER_NOT_FOUND = 'USER_NOT_FOUND',
// Payment/Credit Errors (402)
INSUFFICIENT_CREDITS = 'INSUFFICIENT_CREDITS',
PAYMENT_REQUIRED = 'PAYMENT_REQUIRED',
// Payment/Credit Errors (402)
INSUFFICIENT_CREDITS = 'INSUFFICIENT_CREDITS',
PAYMENT_REQUIRED = 'PAYMENT_REQUIRED',
// Conflict Errors (409)
CONFLICT = 'CONFLICT',
DUPLICATE_ENTRY = 'DUPLICATE_ENTRY',
// Conflict Errors (409)
CONFLICT = 'CONFLICT',
DUPLICATE_ENTRY = 'DUPLICATE_ENTRY',
// Rate Limiting (429)
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
TOO_MANY_REQUESTS = 'TOO_MANY_REQUESTS',
// Rate Limiting (429)
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
TOO_MANY_REQUESTS = 'TOO_MANY_REQUESTS',
// Service Errors (500)
INTERNAL_ERROR = 'INTERNAL_ERROR',
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
GENERATION_FAILED = 'GENERATION_FAILED',
EXTERNAL_SERVICE_ERROR = 'EXTERNAL_SERVICE_ERROR',
// Service Errors (500)
INTERNAL_ERROR = 'INTERNAL_ERROR',
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
GENERATION_FAILED = 'GENERATION_FAILED',
EXTERNAL_SERVICE_ERROR = 'EXTERNAL_SERVICE_ERROR',
// Network Errors (502/503/504)
NETWORK_ERROR = 'NETWORK_ERROR',
TIMEOUT = 'TIMEOUT',
CONNECTION_REFUSED = 'CONNECTION_REFUSED',
// Network Errors (502/503/504)
NETWORK_ERROR = 'NETWORK_ERROR',
TIMEOUT = 'TIMEOUT',
CONNECTION_REFUSED = 'CONNECTION_REFUSED',
// Database Errors
DATABASE_ERROR = 'DATABASE_ERROR',
CONSTRAINT_VIOLATION = 'CONSTRAINT_VIOLATION',
// Database Errors
DATABASE_ERROR = 'DATABASE_ERROR',
CONSTRAINT_VIOLATION = 'CONSTRAINT_VIOLATION',
// Unknown
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
// Unknown
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
}
/**
* Maps error codes to default HTTP status codes.
*/
export const ERROR_CODE_TO_HTTP_STATUS: Record<ErrorCode, number> = {
// Validation (400)
[ErrorCode.VALIDATION_FAILED]: 400,
[ErrorCode.INVALID_INPUT]: 400,
[ErrorCode.MISSING_REQUIRED_FIELD]: 400,
[ErrorCode.INVALID_FORMAT]: 400,
// Validation (400)
[ErrorCode.VALIDATION_FAILED]: 400,
[ErrorCode.INVALID_INPUT]: 400,
[ErrorCode.MISSING_REQUIRED_FIELD]: 400,
[ErrorCode.INVALID_FORMAT]: 400,
// Authentication (401)
[ErrorCode.AUTHENTICATION_REQUIRED]: 401,
[ErrorCode.INVALID_TOKEN]: 401,
[ErrorCode.TOKEN_EXPIRED]: 401,
// Authentication (401)
[ErrorCode.AUTHENTICATION_REQUIRED]: 401,
[ErrorCode.INVALID_TOKEN]: 401,
[ErrorCode.TOKEN_EXPIRED]: 401,
// Authorization (403)
[ErrorCode.PERMISSION_DENIED]: 403,
[ErrorCode.RESOURCE_NOT_OWNED]: 403,
// Authorization (403)
[ErrorCode.PERMISSION_DENIED]: 403,
[ErrorCode.RESOURCE_NOT_OWNED]: 403,
// Not Found (404)
[ErrorCode.RESOURCE_NOT_FOUND]: 404,
[ErrorCode.USER_NOT_FOUND]: 404,
// Not Found (404)
[ErrorCode.RESOURCE_NOT_FOUND]: 404,
[ErrorCode.USER_NOT_FOUND]: 404,
// Payment (402)
[ErrorCode.INSUFFICIENT_CREDITS]: 402,
[ErrorCode.PAYMENT_REQUIRED]: 402,
// Payment (402)
[ErrorCode.INSUFFICIENT_CREDITS]: 402,
[ErrorCode.PAYMENT_REQUIRED]: 402,
// Conflict (409)
[ErrorCode.CONFLICT]: 409,
[ErrorCode.DUPLICATE_ENTRY]: 409,
// Conflict (409)
[ErrorCode.CONFLICT]: 409,
[ErrorCode.DUPLICATE_ENTRY]: 409,
// Rate Limit (429)
[ErrorCode.RATE_LIMIT_EXCEEDED]: 429,
[ErrorCode.TOO_MANY_REQUESTS]: 429,
// Rate Limit (429)
[ErrorCode.RATE_LIMIT_EXCEEDED]: 429,
[ErrorCode.TOO_MANY_REQUESTS]: 429,
// Service Errors (500)
[ErrorCode.INTERNAL_ERROR]: 500,
[ErrorCode.SERVICE_UNAVAILABLE]: 503,
[ErrorCode.GENERATION_FAILED]: 500,
[ErrorCode.EXTERNAL_SERVICE_ERROR]: 502,
// Service Errors (500)
[ErrorCode.INTERNAL_ERROR]: 500,
[ErrorCode.SERVICE_UNAVAILABLE]: 503,
[ErrorCode.GENERATION_FAILED]: 500,
[ErrorCode.EXTERNAL_SERVICE_ERROR]: 502,
// Network Errors
[ErrorCode.NETWORK_ERROR]: 502,
[ErrorCode.TIMEOUT]: 504,
[ErrorCode.CONNECTION_REFUSED]: 503,
// Network Errors
[ErrorCode.NETWORK_ERROR]: 502,
[ErrorCode.TIMEOUT]: 504,
[ErrorCode.CONNECTION_REFUSED]: 503,
// Database Errors
[ErrorCode.DATABASE_ERROR]: 500,
[ErrorCode.CONSTRAINT_VIOLATION]: 409,
// Database Errors
[ErrorCode.DATABASE_ERROR]: 500,
[ErrorCode.CONSTRAINT_VIOLATION]: 409,
// Unknown
[ErrorCode.UNKNOWN_ERROR]: 500,
// Unknown
[ErrorCode.UNKNOWN_ERROR]: 500,
};
/**
* Maps error codes to default retryable status.
*/
export const ERROR_CODE_RETRYABLE: Record<ErrorCode, boolean> = {
// Validation - not retryable (user needs to fix input)
[ErrorCode.VALIDATION_FAILED]: false,
[ErrorCode.INVALID_INPUT]: false,
[ErrorCode.MISSING_REQUIRED_FIELD]: false,
[ErrorCode.INVALID_FORMAT]: false,
// Validation - not retryable (user needs to fix input)
[ErrorCode.VALIDATION_FAILED]: false,
[ErrorCode.INVALID_INPUT]: false,
[ErrorCode.MISSING_REQUIRED_FIELD]: false,
[ErrorCode.INVALID_FORMAT]: false,
// Authentication - not retryable (need new credentials)
[ErrorCode.AUTHENTICATION_REQUIRED]: false,
[ErrorCode.INVALID_TOKEN]: false,
[ErrorCode.TOKEN_EXPIRED]: false,
// Authentication - not retryable (need new credentials)
[ErrorCode.AUTHENTICATION_REQUIRED]: false,
[ErrorCode.INVALID_TOKEN]: false,
[ErrorCode.TOKEN_EXPIRED]: false,
// Authorization - not retryable (permission issue)
[ErrorCode.PERMISSION_DENIED]: false,
[ErrorCode.RESOURCE_NOT_OWNED]: false,
// Authorization - not retryable (permission issue)
[ErrorCode.PERMISSION_DENIED]: false,
[ErrorCode.RESOURCE_NOT_OWNED]: false,
// Not Found - not retryable (resource doesn't exist)
[ErrorCode.RESOURCE_NOT_FOUND]: false,
[ErrorCode.USER_NOT_FOUND]: false,
// Not Found - not retryable (resource doesn't exist)
[ErrorCode.RESOURCE_NOT_FOUND]: false,
[ErrorCode.USER_NOT_FOUND]: false,
// Payment - not retryable (need more credits)
[ErrorCode.INSUFFICIENT_CREDITS]: false,
[ErrorCode.PAYMENT_REQUIRED]: false,
// Payment - not retryable (need more credits)
[ErrorCode.INSUFFICIENT_CREDITS]: false,
[ErrorCode.PAYMENT_REQUIRED]: false,
// Conflict - not retryable (data issue)
[ErrorCode.CONFLICT]: false,
[ErrorCode.DUPLICATE_ENTRY]: false,
// Conflict - not retryable (data issue)
[ErrorCode.CONFLICT]: false,
[ErrorCode.DUPLICATE_ENTRY]: false,
// Rate Limit - retryable (after waiting)
[ErrorCode.RATE_LIMIT_EXCEEDED]: true,
[ErrorCode.TOO_MANY_REQUESTS]: true,
// Rate Limit - retryable (after waiting)
[ErrorCode.RATE_LIMIT_EXCEEDED]: true,
[ErrorCode.TOO_MANY_REQUESTS]: true,
// Service Errors - retryable (transient issues)
[ErrorCode.INTERNAL_ERROR]: true,
[ErrorCode.SERVICE_UNAVAILABLE]: true,
[ErrorCode.GENERATION_FAILED]: true,
[ErrorCode.EXTERNAL_SERVICE_ERROR]: true,
// Service Errors - retryable (transient issues)
[ErrorCode.INTERNAL_ERROR]: true,
[ErrorCode.SERVICE_UNAVAILABLE]: true,
[ErrorCode.GENERATION_FAILED]: true,
[ErrorCode.EXTERNAL_SERVICE_ERROR]: true,
// Network Errors - retryable (transient issues)
[ErrorCode.NETWORK_ERROR]: true,
[ErrorCode.TIMEOUT]: true,
[ErrorCode.CONNECTION_REFUSED]: true,
// Network Errors - retryable (transient issues)
[ErrorCode.NETWORK_ERROR]: true,
[ErrorCode.TIMEOUT]: true,
[ErrorCode.CONNECTION_REFUSED]: true,
// Database Errors - not retryable (except transient, but safer to say no)
[ErrorCode.DATABASE_ERROR]: false,
[ErrorCode.CONSTRAINT_VIOLATION]: false,
// Database Errors - not retryable (except transient, but safer to say no)
[ErrorCode.DATABASE_ERROR]: false,
[ErrorCode.CONSTRAINT_VIOLATION]: false,
// Unknown - retryable (might be transient)
[ErrorCode.UNKNOWN_ERROR]: true,
// Unknown - retryable (might be transient)
[ErrorCode.UNKNOWN_ERROR]: true,
};

View file

@ -25,8 +25,8 @@ import { ErrorCode } from './error-codes';
* ```
*/
export type Result<T, E extends AppError = AppError> =
| { readonly ok: true; readonly value: T; readonly error?: never }
| { readonly ok: false; readonly error: E; readonly value?: never };
| { readonly ok: true; readonly value: T; readonly error?: never }
| { readonly ok: false; readonly error: E; readonly value?: never };
/**
* Async version of Result - use this as return type for async functions.
@ -42,7 +42,7 @@ export type AsyncResult<T, E extends AppError = AppError> = Promise<Result<T, E>
* ```
*/
export function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
return { ok: true, value };
}
/**
@ -55,7 +55,7 @@ export function ok<T>(value: T): Result<T, never> {
* ```
*/
export function err<E extends AppError>(error: E): Result<never, E> {
return { ok: false, error };
return { ok: false, error };
}
/**
@ -71,9 +71,9 @@ export function err<E extends AppError>(error: E): Result<never, E> {
* ```
*/
export function isOk<T, E extends AppError>(
result: Result<T, E>
result: Result<T, E>
): result is { ok: true; value: T } {
return result.ok === true;
return result.ok === true;
}
/**
@ -89,9 +89,9 @@ export function isOk<T, E extends AppError>(
* ```
*/
export function isErr<T, E extends AppError>(
result: Result<T, E>
result: Result<T, E>
): result is { ok: false; error: E } {
return result.ok === false;
return result.ok === false;
}
/**
@ -107,10 +107,10 @@ export function isErr<T, E extends AppError>(
* ```
*/
export function unwrap<T, E extends AppError>(result: Result<T, E>): T {
if (isOk(result)) {
return result.value;
}
throw result.error;
if (isOk(result)) {
return result.value;
}
throw result.error;
}
/**
@ -121,11 +121,8 @@ export function unwrap<T, E extends AppError>(result: Result<T, E>): T {
* const users = unwrapOr(result, []); // Returns [] if error
* ```
*/
export function unwrapOr<T, E extends AppError>(
result: Result<T, E>,
defaultValue: T
): T {
return isOk(result) ? result.value : defaultValue;
export function unwrapOr<T, E extends AppError>(result: Result<T, E>, defaultValue: T): T {
return isOk(result) ? result.value : defaultValue;
}
/**
@ -139,11 +136,8 @@ export function unwrapOr<T, E extends AppError>(
* });
* ```
*/
export function unwrapOrElse<T, E extends AppError>(
result: Result<T, E>,
fn: (error: E) => T
): T {
return isOk(result) ? result.value : fn(result.error);
export function unwrapOrElse<T, E extends AppError>(result: Result<T, E>, fn: (error: E) => T): T {
return isOk(result) ? result.value : fn(result.error);
}
/**
@ -156,10 +150,10 @@ export function unwrapOrElse<T, E extends AppError>(
* ```
*/
export function map<T, U, E extends AppError>(
result: Result<T, E>,
fn: (value: T) => U
result: Result<T, E>,
fn: (value: T) => U
): Result<U, E> {
return isOk(result) ? ok(fn(result.value)) : result;
return isOk(result) ? ok(fn(result.value)) : result;
}
/**
@ -173,10 +167,10 @@ export function map<T, U, E extends AppError>(
* ```
*/
export function mapErr<T, E extends AppError, F extends AppError>(
result: Result<T, E>,
fn: (error: E) => F
result: Result<T, E>,
fn: (error: E) => F
): Result<T, F> {
return isErr(result) ? err(fn(result.error)) : result;
return isErr(result) ? err(fn(result.error)) : result;
}
/**
@ -190,10 +184,10 @@ export function mapErr<T, E extends AppError, F extends AppError>(
* ```
*/
export function andThen<T, U, E extends AppError>(
result: Result<T, E>,
fn: (value: T) => Result<U, E>
result: Result<T, E>,
fn: (value: T) => Result<U, E>
): Result<U, E> {
return isOk(result) ? fn(result.value) : result;
return isOk(result) ? fn(result.value) : result;
}
/**
@ -208,13 +202,13 @@ export function andThen<T, U, E extends AppError>(
* ```
*/
export function match<T, E extends AppError, U>(
result: Result<T, E>,
handlers: {
ok: (value: T) => U;
err: (error: E) => U;
}
result: Result<T, E>,
handlers: {
ok: (value: T) => U;
err: (error: E) => U;
}
): U {
return isOk(result) ? handlers.ok(result.value) : handlers.err(result.error);
return isOk(result) ? handlers.ok(result.value) : handlers.err(result.error);
}
/**
@ -226,20 +220,20 @@ export function match<T, E extends AppError, U>(
* ```
*/
export function tryCatch<T>(fn: () => T): Result<T, AppError> {
try {
return ok(fn());
} catch (error) {
if (error instanceof AppError) {
return err(error);
}
return err(
new AppError({
code: ErrorCode.UNKNOWN_ERROR,
message: error instanceof Error ? error.message : String(error),
cause: error instanceof Error ? error : undefined,
})
);
}
try {
return ok(fn());
} catch (error) {
if (error instanceof AppError) {
return err(error);
}
return err(
new AppError({
code: ErrorCode.UNKNOWN_ERROR,
message: error instanceof Error ? error.message : String(error),
cause: error instanceof Error ? error : undefined,
})
);
}
}
/**
@ -250,23 +244,21 @@ export function tryCatch<T>(fn: () => T): Result<T, AppError> {
* const result = await tryCatchAsync(() => fetch(url).then(r => r.json()));
* ```
*/
export async function tryCatchAsync<T>(
fn: () => Promise<T>
): AsyncResult<T, AppError> {
try {
return ok(await fn());
} catch (error) {
if (error instanceof AppError) {
return err(error);
}
return err(
new AppError({
code: ErrorCode.UNKNOWN_ERROR,
message: error instanceof Error ? error.message : String(error),
cause: error instanceof Error ? error : undefined,
})
);
}
export async function tryCatchAsync<T>(fn: () => Promise<T>): AsyncResult<T, AppError> {
try {
return ok(await fn());
} catch (error) {
if (error instanceof AppError) {
return err(error);
}
return err(
new AppError({
code: ErrorCode.UNKNOWN_ERROR,
message: error instanceof Error ? error.message : String(error),
cause: error instanceof Error ? error : undefined,
})
);
}
}
/**
@ -285,17 +277,15 @@ export async function tryCatchAsync<T>(
* }
* ```
*/
export function combine<T, E extends AppError>(
results: Result<T, E>[]
): Result<T[], E> {
const values: T[] = [];
for (const result of results) {
if (isErr(result)) {
return result;
}
values.push(result.value);
}
return ok(values);
export function combine<T, E extends AppError>(results: Result<T, E>[]): Result<T[], E> {
const values: T[] = [];
for (const result of results) {
if (isErr(result)) {
return result;
}
values.push(result.value);
}
return ok(values);
}
/**
@ -310,10 +300,10 @@ export function combine<T, E extends AppError>(
* ```
*/
export function fromNullable<T, E extends AppError>(
value: T | null | undefined,
errorFn: () => E
value: T | null | undefined,
errorFn: () => E
): Result<T, E> {
return value != null ? ok(value) : err(errorFn());
return value != null ? ok(value) : err(errorFn());
}
/**
@ -324,8 +314,6 @@ export function fromNullable<T, E extends AppError>(
* const user = toNullable(result); // User | null
* ```
*/
export function toNullable<T, E extends AppError>(
result: Result<T, E>
): T | null {
return isOk(result) ? result.value : null;
export function toNullable<T, E extends AppError>(result: Result<T, E>): T | null {
return isOk(result) ? result.value : null;
}

View file

@ -20,22 +20,18 @@ import { isAppError } from '../guards/type-guards';
* }
* ```
*/
export function wrap(
error: unknown,
context: string,
additionalContext?: ErrorContext
): AppError {
if (isAppError(error)) {
return error.wrap(context, additionalContext);
}
export function wrap(error: unknown, context: string, additionalContext?: ErrorContext): AppError {
if (isAppError(error)) {
return error.wrap(context, additionalContext);
}
const message = error instanceof Error ? error.message : String(error);
return new AppError({
code: ErrorCode.UNKNOWN_ERROR,
message: `${context}: ${message}`,
cause: error instanceof Error ? error : undefined,
context: additionalContext,
});
const message = error instanceof Error ? error.message : String(error);
return new AppError({
code: ErrorCode.UNKNOWN_ERROR,
message: `${context}: ${message}`,
cause: error instanceof Error ? error : undefined,
context: additionalContext,
});
}
/**
@ -52,22 +48,22 @@ export function wrap(
* ```
*/
export function toAppError(error: unknown): AppError {
if (isAppError(error)) {
return error;
}
if (isAppError(error)) {
return error;
}
if (error instanceof Error) {
return new AppError({
code: ErrorCode.UNKNOWN_ERROR,
message: error.message,
cause: error,
});
}
if (error instanceof Error) {
return new AppError({
code: ErrorCode.UNKNOWN_ERROR,
message: error.message,
cause: error,
});
}
return new AppError({
code: ErrorCode.UNKNOWN_ERROR,
message: String(error),
});
return new AppError({
code: ErrorCode.UNKNOWN_ERROR,
message: String(error),
});
}
/**
@ -79,7 +75,7 @@ export function toAppError(error: unknown): AppError {
* ```
*/
export function cause(error: AppError): Error | undefined {
return error.cause;
return error.cause;
}
/**
@ -92,5 +88,5 @@ export function cause(error: AppError): Error | undefined {
* ```
*/
export function rootCause(error: AppError): Error {
return error.rootCause();
return error.rootCause();
}