@geekmidas/errors 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +500 -0
- package/dist/index.cjs +586 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +534 -0
- package/dist/index.d.mts +534 -0
- package/dist/index.mjs +564 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +22 -0
- package/src/__tests__/errors.spec.ts +591 -0
- package/src/index.ts +829 -0
- package/tsdown.config.ts +5 -0
package/README.md
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
# @geekmidas/errors
|
|
2
|
+
|
|
3
|
+
Type-safe HTTP error classes with full TypeScript support, providing structured error handling for REST APIs and HTTP-based applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Type-Safe Errors**: Full TypeScript support with proper error hierarchies
|
|
8
|
+
- **HTTP Status Codes**: Pre-built error classes for all common HTTP errors
|
|
9
|
+
- **Structured Error Details**: Include additional context and debugging information
|
|
10
|
+
- **Error Factories**: Convenient factory functions for creating errors
|
|
11
|
+
- **Type Guards**: Runtime type checking for error instances
|
|
12
|
+
- **JSON Serialization**: Built-in support for error serialization
|
|
13
|
+
- **Error Wrapping**: Wrap unknown errors into HTTP errors
|
|
14
|
+
- **Cause Chaining**: ES2022 error cause support for error chains
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add @geekmidas/errors
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### Basic Usage
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import {
|
|
28
|
+
BadRequestError,
|
|
29
|
+
NotFoundError,
|
|
30
|
+
UnauthorizedError,
|
|
31
|
+
InternalServerError
|
|
32
|
+
} from '@geekmidas/errors';
|
|
33
|
+
|
|
34
|
+
// Throw specific error types
|
|
35
|
+
throw new NotFoundError('User not found');
|
|
36
|
+
throw new BadRequestError('Invalid email format');
|
|
37
|
+
throw new UnauthorizedError('Invalid token');
|
|
38
|
+
throw new InternalServerError('Database connection failed');
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### With Error Details
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { BadRequestError, NotFoundError } from '@geekmidas/errors';
|
|
45
|
+
|
|
46
|
+
// Include additional context
|
|
47
|
+
throw new NotFoundError('User not found', { userId: '123' });
|
|
48
|
+
|
|
49
|
+
throw new BadRequestError('Validation failed', {
|
|
50
|
+
field: 'email',
|
|
51
|
+
value: 'invalid-email',
|
|
52
|
+
message: 'Must be a valid email address'
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Available Error Classes
|
|
57
|
+
|
|
58
|
+
### Client Errors (4xx)
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import {
|
|
62
|
+
BadRequestError, // 400
|
|
63
|
+
UnauthorizedError, // 401
|
|
64
|
+
ForbiddenError, // 403
|
|
65
|
+
NotFoundError, // 404
|
|
66
|
+
MethodNotAllowedError, // 405
|
|
67
|
+
ConflictError, // 409
|
|
68
|
+
UnprocessableEntityError, // 422
|
|
69
|
+
TooManyRequestsError // 429
|
|
70
|
+
} from '@geekmidas/errors';
|
|
71
|
+
|
|
72
|
+
// 400 Bad Request
|
|
73
|
+
throw new BadRequestError('Invalid input');
|
|
74
|
+
|
|
75
|
+
// 401 Unauthorized
|
|
76
|
+
throw new UnauthorizedError('Authentication required');
|
|
77
|
+
|
|
78
|
+
// 403 Forbidden
|
|
79
|
+
throw new ForbiddenError('Insufficient permissions', {
|
|
80
|
+
required: 'admin',
|
|
81
|
+
current: 'user'
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// 404 Not Found
|
|
85
|
+
throw new NotFoundError('Resource not found');
|
|
86
|
+
|
|
87
|
+
// 405 Method Not Allowed
|
|
88
|
+
throw new MethodNotAllowedError('DELETE not supported', ['GET', 'POST', 'PUT']);
|
|
89
|
+
|
|
90
|
+
// 409 Conflict
|
|
91
|
+
throw new ConflictError('Email already exists', {
|
|
92
|
+
email: 'user@example.com'
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// 422 Unprocessable Entity
|
|
96
|
+
throw new UnprocessableEntityError('Validation failed', {
|
|
97
|
+
email: 'Invalid email format',
|
|
98
|
+
age: 'Must be 18 or older'
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// 429 Too Many Requests
|
|
102
|
+
throw new TooManyRequestsError('Rate limit exceeded', 60); // retry after 60 seconds
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Server Errors (5xx)
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import {
|
|
109
|
+
InternalServerError, // 500
|
|
110
|
+
NotImplementedError, // 501
|
|
111
|
+
BadGatewayError, // 502
|
|
112
|
+
ServiceUnavailableError, // 503
|
|
113
|
+
GatewayTimeoutError // 504
|
|
114
|
+
} from '@geekmidas/errors';
|
|
115
|
+
|
|
116
|
+
// 500 Internal Server Error
|
|
117
|
+
throw new InternalServerError('Database connection failed');
|
|
118
|
+
|
|
119
|
+
// 501 Not Implemented
|
|
120
|
+
throw new NotImplementedError('Feature not yet implemented');
|
|
121
|
+
|
|
122
|
+
// 502 Bad Gateway
|
|
123
|
+
throw new BadGatewayError('Upstream server error');
|
|
124
|
+
|
|
125
|
+
// 503 Service Unavailable
|
|
126
|
+
throw new ServiceUnavailableError('Maintenance in progress', 300); // retry after 5 minutes
|
|
127
|
+
|
|
128
|
+
// 504 Gateway Timeout
|
|
129
|
+
throw new GatewayTimeoutError('Upstream server timeout');
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Error Factories
|
|
133
|
+
|
|
134
|
+
Use convenient factory functions for creating errors:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import { createError } from '@geekmidas/errors';
|
|
138
|
+
|
|
139
|
+
// Short, descriptive method names
|
|
140
|
+
throw createError.badRequest('Invalid input');
|
|
141
|
+
throw createError.notFound('User not found');
|
|
142
|
+
throw createError.unauthorized('Invalid token');
|
|
143
|
+
throw createError.forbidden('Access denied');
|
|
144
|
+
throw createError.conflict('Resource already exists');
|
|
145
|
+
throw createError.internalServerError('Something went wrong');
|
|
146
|
+
|
|
147
|
+
// With details
|
|
148
|
+
throw createError.notFound('User not found', { userId: '123' });
|
|
149
|
+
throw createError.badRequest('Validation failed', {
|
|
150
|
+
errors: ['Invalid email', 'Password too short']
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Generic Error Factory
|
|
155
|
+
|
|
156
|
+
Create errors dynamically with type-safe options:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { createHttpError } from '@geekmidas/errors';
|
|
160
|
+
|
|
161
|
+
// TypeScript knows which options are valid for each status code
|
|
162
|
+
throw createHttpError(404, 'Not found');
|
|
163
|
+
throw createHttpError(429, 'Rate limited', { retryAfter: 60 });
|
|
164
|
+
throw createHttpError(422, 'Validation failed', {
|
|
165
|
+
validationErrors: {
|
|
166
|
+
email: 'Invalid format',
|
|
167
|
+
age: 'Must be 18+'
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Type Guards
|
|
173
|
+
|
|
174
|
+
Check error types at runtime:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import {
|
|
178
|
+
isHttpError,
|
|
179
|
+
isClientError,
|
|
180
|
+
isServerError,
|
|
181
|
+
NotFoundError
|
|
182
|
+
} from '@geekmidas/errors';
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
// some code
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (isHttpError(error)) {
|
|
188
|
+
console.log(`HTTP ${error.statusCode}: ${error.message}`);
|
|
189
|
+
console.log('Details:', error.details);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (isClientError(error)) {
|
|
193
|
+
// 4xx errors - client's fault
|
|
194
|
+
console.log('Client error:', error.message);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (isServerError(error)) {
|
|
198
|
+
// 5xx errors - server's fault
|
|
199
|
+
console.error('Server error:', error.message);
|
|
200
|
+
// Alert monitoring system
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (error instanceof NotFoundError) {
|
|
204
|
+
// Specific error type
|
|
205
|
+
console.log('Resource not found');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Error Wrapping
|
|
211
|
+
|
|
212
|
+
Wrap unknown errors into HTTP errors:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { wrapError } from '@geekmidas/errors';
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
await someOperation();
|
|
219
|
+
} catch (error) {
|
|
220
|
+
// Wrap unknown error as 500
|
|
221
|
+
throw wrapError(error);
|
|
222
|
+
|
|
223
|
+
// Or wrap with specific status and message
|
|
224
|
+
throw wrapError(error, 503, 'Service temporarily unavailable');
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Error Serialization
|
|
229
|
+
|
|
230
|
+
Serialize errors to JSON:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import { NotFoundError } from '@geekmidas/errors';
|
|
234
|
+
|
|
235
|
+
const error = new NotFoundError('User not found', { userId: '123' });
|
|
236
|
+
|
|
237
|
+
// Get JSON representation
|
|
238
|
+
const json = error.toJSON();
|
|
239
|
+
// {
|
|
240
|
+
// name: 'NotFoundError',
|
|
241
|
+
// message: 'User not found',
|
|
242
|
+
// statusCode: 404,
|
|
243
|
+
// statusMessage: 'Not Found',
|
|
244
|
+
// details: { userId: '123' },
|
|
245
|
+
// stack: '...'
|
|
246
|
+
// }
|
|
247
|
+
|
|
248
|
+
// Get error body for HTTP response
|
|
249
|
+
const body = error.body;
|
|
250
|
+
// JSON string: '{"message":"User not found","details":{"userId":"123"}}'
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Error Cause Chaining
|
|
254
|
+
|
|
255
|
+
Chain errors using ES2022 error cause:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
import { InternalServerError } from '@geekmidas/errors';
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
await database.connect();
|
|
262
|
+
} catch (originalError) {
|
|
263
|
+
throw new InternalServerError('Failed to connect to database', {
|
|
264
|
+
cause: originalError,
|
|
265
|
+
details: { host: 'localhost', port: 5432 }
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Express Middleware
|
|
271
|
+
|
|
272
|
+
Handle errors in Express applications:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { isHttpError } from '@geekmidas/errors';
|
|
276
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
277
|
+
|
|
278
|
+
function errorHandler(
|
|
279
|
+
error: unknown,
|
|
280
|
+
req: Request,
|
|
281
|
+
res: Response,
|
|
282
|
+
next: NextFunction
|
|
283
|
+
) {
|
|
284
|
+
if (isHttpError(error)) {
|
|
285
|
+
res.status(error.statusCode).json({
|
|
286
|
+
error: {
|
|
287
|
+
message: error.message,
|
|
288
|
+
code: error.code,
|
|
289
|
+
details: error.details
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
} else {
|
|
293
|
+
// Unknown error
|
|
294
|
+
console.error('Unexpected error:', error);
|
|
295
|
+
res.status(500).json({
|
|
296
|
+
error: {
|
|
297
|
+
message: 'Internal Server Error'
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Use in Express app
|
|
304
|
+
app.use(errorHandler);
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Hono Middleware
|
|
308
|
+
|
|
309
|
+
Handle errors in Hono applications:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
import { Hono } from 'hono';
|
|
313
|
+
import { isHttpError } from '@geekmidas/errors';
|
|
314
|
+
|
|
315
|
+
const app = new Hono();
|
|
316
|
+
|
|
317
|
+
app.onError((error, c) => {
|
|
318
|
+
if (isHttpError(error)) {
|
|
319
|
+
return c.json(
|
|
320
|
+
{
|
|
321
|
+
error: {
|
|
322
|
+
message: error.message,
|
|
323
|
+
code: error.code,
|
|
324
|
+
details: error.details
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
error.statusCode
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
console.error('Unexpected error:', error);
|
|
332
|
+
return c.json(
|
|
333
|
+
{ error: { message: 'Internal Server Error' } },
|
|
334
|
+
500
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## Custom Error Classes
|
|
340
|
+
|
|
341
|
+
Extend base classes for custom errors:
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
import { HttpError } from '@geekmidas/errors';
|
|
345
|
+
|
|
346
|
+
export class CustomBusinessError extends HttpError {
|
|
347
|
+
constructor(message?: string, details?: any) {
|
|
348
|
+
super(400, message, { details, code: 'BUSINESS_RULE_VIOLATION' });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Usage
|
|
353
|
+
throw new CustomBusinessError('Cannot delete user with active orders', {
|
|
354
|
+
userId: '123',
|
|
355
|
+
activeOrders: 5
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## Error Context
|
|
360
|
+
|
|
361
|
+
Add context to errors for better debugging:
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
import { NotFoundError } from '@geekmidas/errors';
|
|
365
|
+
|
|
366
|
+
function getUserById(id: string) {
|
|
367
|
+
const user = database.findUser(id);
|
|
368
|
+
|
|
369
|
+
if (!user) {
|
|
370
|
+
throw new NotFoundError('User not found', {
|
|
371
|
+
userId: id,
|
|
372
|
+
requestId: req.headers['x-request-id'],
|
|
373
|
+
timestamp: new Date().toISOString(),
|
|
374
|
+
source: 'getUserById'
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return user;
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## HTTP Status Codes
|
|
383
|
+
|
|
384
|
+
Use the HttpStatusCode enum for type-safe status codes:
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
import { HttpStatusCode } from '@geekmidas/errors';
|
|
388
|
+
|
|
389
|
+
// Success
|
|
390
|
+
HttpStatusCode.OK // 200
|
|
391
|
+
HttpStatusCode.CREATED // 201
|
|
392
|
+
HttpStatusCode.NO_CONTENT // 204
|
|
393
|
+
|
|
394
|
+
// Redirection
|
|
395
|
+
HttpStatusCode.MOVED_PERMANENTLY // 301
|
|
396
|
+
HttpStatusCode.NOT_MODIFIED // 304
|
|
397
|
+
|
|
398
|
+
// Client Errors
|
|
399
|
+
HttpStatusCode.BAD_REQUEST // 400
|
|
400
|
+
HttpStatusCode.UNAUTHORIZED // 401
|
|
401
|
+
HttpStatusCode.FORBIDDEN // 403
|
|
402
|
+
HttpStatusCode.NOT_FOUND // 404
|
|
403
|
+
HttpStatusCode.CONFLICT // 409
|
|
404
|
+
HttpStatusCode.UNPROCESSABLE_ENTITY // 422
|
|
405
|
+
HttpStatusCode.TOO_MANY_REQUESTS // 429
|
|
406
|
+
|
|
407
|
+
// Server Errors
|
|
408
|
+
HttpStatusCode.INTERNAL_SERVER_ERROR // 500
|
|
409
|
+
HttpStatusCode.SERVICE_UNAVAILABLE // 503
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Best Practices
|
|
413
|
+
|
|
414
|
+
### 1. Use Specific Error Classes
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// ❌ Don't use generic Error
|
|
418
|
+
throw new Error('User not found');
|
|
419
|
+
|
|
420
|
+
// ✅ Use specific HTTP error
|
|
421
|
+
throw new NotFoundError('User not found', { userId: '123' });
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### 2. Include Helpful Details
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
// ❌ Minimal information
|
|
428
|
+
throw new BadRequestError('Invalid input');
|
|
429
|
+
|
|
430
|
+
// ✅ Include context
|
|
431
|
+
throw new BadRequestError('Invalid email format', {
|
|
432
|
+
field: 'email',
|
|
433
|
+
value: userInput,
|
|
434
|
+
expectedFormat: 'user@example.com'
|
|
435
|
+
});
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### 3. Use Type Guards
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
// ❌ Assume error type
|
|
442
|
+
catch (error: any) {
|
|
443
|
+
console.log(error.statusCode); // Unsafe
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ✅ Check error type
|
|
447
|
+
catch (error) {
|
|
448
|
+
if (isHttpError(error)) {
|
|
449
|
+
console.log(error.statusCode); // Safe
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### 4. Chain Errors
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
// ✅ Preserve original error context
|
|
458
|
+
try {
|
|
459
|
+
await database.query();
|
|
460
|
+
} catch (originalError) {
|
|
461
|
+
throw new InternalServerError('Database query failed', {
|
|
462
|
+
cause: originalError,
|
|
463
|
+
query: 'SELECT * FROM users'
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## TypeScript Types
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
import type {
|
|
472
|
+
HttpError,
|
|
473
|
+
HttpErrorOptions,
|
|
474
|
+
HttpErrorConstructor
|
|
475
|
+
} from '@geekmidas/errors';
|
|
476
|
+
|
|
477
|
+
// Options for creating errors
|
|
478
|
+
interface HttpErrorOptions {
|
|
479
|
+
statusMessage?: string;
|
|
480
|
+
details?: any;
|
|
481
|
+
code?: string;
|
|
482
|
+
cause?: Error;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Constructor type for factory patterns
|
|
486
|
+
type HttpErrorConstructor = new (
|
|
487
|
+
message?: string,
|
|
488
|
+
options?: HttpErrorOptions
|
|
489
|
+
) => HttpError;
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## Related Packages
|
|
493
|
+
|
|
494
|
+
- [@geekmidas/constructs](../constructs) - Uses these error classes in endpoints
|
|
495
|
+
- [@geekmidas/client](../client) - Handles these errors on the client side
|
|
496
|
+
- [@geekmidas/logger](../logger) - Log errors with structured context
|
|
497
|
+
|
|
498
|
+
## License
|
|
499
|
+
|
|
500
|
+
MIT
|