errors.ts74 lines · main
1/**
2 * Base error class for every error thrown inside briven services.
3 * Per CLAUDE.md §6.3: never throw raw strings, always use a code.
4 *
5 * The public sanitiser strips internal paths, IPs, and credentials before
6 * a customer-facing surface sees the error. Do that at boundaries, never in
7 * the constructor.
8 */
9export class brivenError extends Error {
10 readonly code: string;
11 readonly status: number;
12 readonly cause?: unknown;
13 readonly context?: Readonly<Record<string, unknown>>;
14
15 constructor(
16 code: string,
17 message: string,
18 options: { status?: number; cause?: unknown; context?: Record<string, unknown> } = {},
19 ) {
20 super(message);
21 this.name = 'brivenError';
22 this.code = code;
23 this.status = options.status ?? 500;
24 this.cause = options.cause;
25 this.context = options.context ? Object.freeze({ ...options.context }) : undefined;
26 }
27
28 toJSON(): { code: string; message: string; status: number } {
29 return { code: this.code, message: this.message, status: this.status };
30 }
31}
32
33export class NotFoundError extends brivenError {
34 constructor(resource: string, id: string) {
35 super('not_found', `${resource} not found`, { status: 404, context: { resource, id } });
36 this.name = 'NotFoundError';
37 }
38}
39
40export class UnauthorizedError extends brivenError {
41 constructor(message = 'authentication required') {
42 super('unauthorized', message, { status: 401 });
43 this.name = 'UnauthorizedError';
44 }
45}
46
47export class ForbiddenError extends brivenError {
48 // The optional second arg lets a caller surface a more specific
49 // machine-readable code (e.g. 'project_suspended', 'tier_required')
50 // while still defaulting to the generic 'forbidden'. The HTTP status
51 // is always 403 — this only affects the error.code field that
52 // clients can branch on.
53 constructor(message = 'forbidden', code = 'forbidden') {
54 super(code, message, { status: 403 });
55 this.name = 'ForbiddenError';
56 }
57}
58
59export class ValidationError extends brivenError {
60 constructor(message: string, context?: Record<string, unknown>) {
61 super('validation_failed', message, { status: 400, context });
62 this.name = 'ValidationError';
63 }
64}
65
66export class RateLimitedError extends brivenError {
67 constructor(retryAfterSeconds: number) {
68 super('rate_limited', 'rate limit exceeded', {
69 status: 429,
70 context: { retryAfterSeconds },
71 });
72 this.name = 'RateLimitedError';
73 }
74}