@nextrush/errors 3.0.0
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/LICENSE +21 -0
- package/README.md +681 -0
- package/dist/index.d.ts +583 -0
- package/dist/index.js +1021 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
- package/src/__tests__/base.test.ts +180 -0
- package/src/__tests__/factory.test.ts +312 -0
- package/src/__tests__/http-errors.test.ts +458 -0
- package/src/__tests__/middleware.test.ts +385 -0
- package/src/__tests__/validation.test.ts +315 -0
- package/src/base.ts +162 -0
- package/src/factory.ts +205 -0
- package/src/http-errors.ts +409 -0
- package/src/index.ts +101 -0
- package/src/middleware.ts +183 -0
- package/src/validation.ts +210 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tanzim (NextRush)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
# @nextrush/errors
|
|
2
|
+
|
|
3
|
+
> Standardized HTTP error handling that eliminates response inconsistency and builds API client trust.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
Every API returns errors differently. This creates chaos for both developers and API consumers:
|
|
8
|
+
|
|
9
|
+
**Inconsistent error responses** plague every backend project. One endpoint returns `{error: "..."}`, another returns `{message: "..."}`, and a third leaks stack traces in production. API clients can't reliably handle errors because there's no standard format.
|
|
10
|
+
|
|
11
|
+
**Internal errors leak to users.** A database connection timeout becomes `PostgreSQL connection failed on pool 'primary'` in the API response. Security researchers see your infrastructure. Users see confusing technical jargon instead of actionable messages.
|
|
12
|
+
|
|
13
|
+
**Manual error formatting is tedious.** Every route handler manually sets status codes, constructs JSON responses, and decides what to expose. Copy-paste error handling leads to bugs. Forgetting `try-catch` crashes the server.
|
|
14
|
+
|
|
15
|
+
**No programmatic error handling.** API clients resort to parsing error messages with regex because there are no stable error codes. A typo in an error message breaks production integrations.
|
|
16
|
+
|
|
17
|
+
## How NextRush Approaches This
|
|
18
|
+
|
|
19
|
+
NextRush treats **errors as API contracts**, not exceptions.
|
|
20
|
+
|
|
21
|
+
Every error has three responsibilities:
|
|
22
|
+
|
|
23
|
+
1. **HTTP status code** - Semantic meaning for browsers and clients
|
|
24
|
+
2. **Human message** - Clear explanation for developers/users
|
|
25
|
+
3. **Machine code** - Stable identifier for programmatic handling
|
|
26
|
+
|
|
27
|
+
The framework distinguishes between **client-safe errors** (4xx) and **server-internal errors** (5xx) with an `expose` flag. Client errors show detailed messages. Server errors hide implementation details by default.
|
|
28
|
+
|
|
29
|
+
All errors serialize to a consistent JSON format automatically. No manual response formatting. No leaked stack traces. No security risks.
|
|
30
|
+
|
|
31
|
+
## Mental Model
|
|
32
|
+
|
|
33
|
+
Think of errors as **structured API responses**, not crashes.
|
|
34
|
+
|
|
35
|
+
### Errors Are Contracts
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
User Request → Handler Logic → Error Thrown → Middleware Catches → JSON Response
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
When you throw `NotFoundError`, you're declaring an API contract:
|
|
42
|
+
|
|
43
|
+
- **Status:** 404 Not Found
|
|
44
|
+
- **Code:** `NOT_FOUND`
|
|
45
|
+
- **Message:** Custom message you provide
|
|
46
|
+
- **Format:** Consistent JSON structure
|
|
47
|
+
|
|
48
|
+
### The Expose Flag
|
|
49
|
+
|
|
50
|
+
Every error has an `expose` flag that acts as a **privacy boundary**:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// Client errors (4xx): expose = true by default
|
|
54
|
+
throw new NotFoundError('User #123 not found');
|
|
55
|
+
// → Client sees: {"message": "User #123 not found", "code": "NOT_FOUND"}
|
|
56
|
+
|
|
57
|
+
// Server errors (5xx): expose = false by default
|
|
58
|
+
throw new InternalServerError('Redis connection timeout');
|
|
59
|
+
// → Client sees: {"message": "Internal Server Error", "code": "INTERNAL_ERROR"}
|
|
60
|
+
// → Server logs: Full error with stack trace
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This prevents security leaks while maintaining debuggability.
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pnpm add @nextrush/errors
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { createApp } from '@nextrush/core';
|
|
75
|
+
import { errorHandler, NotFoundError, BadRequestError } from '@nextrush/errors';
|
|
76
|
+
|
|
77
|
+
const app = createApp();
|
|
78
|
+
|
|
79
|
+
// Add error handling middleware FIRST
|
|
80
|
+
app.use(errorHandler());
|
|
81
|
+
|
|
82
|
+
app.get('/users/:id', (ctx) => {
|
|
83
|
+
const user = users.get(ctx.params.id);
|
|
84
|
+
if (!user) {
|
|
85
|
+
throw new NotFoundError('User not found');
|
|
86
|
+
}
|
|
87
|
+
ctx.json(user);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
app.post('/users', (ctx) => {
|
|
91
|
+
if (!ctx.body.email) {
|
|
92
|
+
throw new BadRequestError('Email is required');
|
|
93
|
+
}
|
|
94
|
+
// Create user...
|
|
95
|
+
ctx.json({ success: true });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Request: GET /users/999
|
|
99
|
+
// Response: 404 Not Found
|
|
100
|
+
// {
|
|
101
|
+
// "error": "NotFoundError",
|
|
102
|
+
// "message": "User not found",
|
|
103
|
+
// "code": "NOT_FOUND",
|
|
104
|
+
// "status": 404
|
|
105
|
+
// }
|
|
106
|
+
|
|
107
|
+
// Request: POST /users (no email)
|
|
108
|
+
// Response: 400 Bad Request
|
|
109
|
+
// {
|
|
110
|
+
// "error": "BadRequestError",
|
|
111
|
+
// "message": "Email is required",
|
|
112
|
+
// "code": "BAD_REQUEST",
|
|
113
|
+
// "status": 400
|
|
114
|
+
// }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## What NextRush Does Automatically
|
|
118
|
+
|
|
119
|
+
When you throw an `HttpError` with error middleware enabled:
|
|
120
|
+
|
|
121
|
+
1. **Catches the error** - No uncaught exceptions crash your server
|
|
122
|
+
2. **Sets HTTP status** - Correct status code from error class
|
|
123
|
+
3. **Formats JSON response** - Consistent `{error, message, code, status}` structure
|
|
124
|
+
4. **Applies expose flag** - Hides sensitive 5xx details, shows 4xx details
|
|
125
|
+
5. **Logs appropriately** - 5xx logged as errors, 4xx as warnings
|
|
126
|
+
6. **Preserves stack traces** - Full debugging in development, hidden in production
|
|
127
|
+
|
|
128
|
+
You don't write error handling code. You **declare error states** and NextRush handles the rest.
|
|
129
|
+
|
|
130
|
+
## Features
|
|
131
|
+
|
|
132
|
+
- **Type-Safe Errors**: Full TypeScript support with proper error inheritance
|
|
133
|
+
- **HTTP Status Codes**: All standard 4xx and 5xx errors included
|
|
134
|
+
- **Validation Errors**: Built-in validation error types
|
|
135
|
+
- **Factory Functions**: Quick error creation with `badRequest()`, `notFound()`, etc.
|
|
136
|
+
- **Error Middleware**: Automatic error response formatting
|
|
137
|
+
- **Error Codes**: Custom error codes for API consumers
|
|
138
|
+
|
|
139
|
+
## HTTP Errors
|
|
140
|
+
|
|
141
|
+
### 4xx Client Errors
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import {
|
|
145
|
+
BadRequestError, // 400
|
|
146
|
+
UnauthorizedError, // 401
|
|
147
|
+
PaymentRequiredError, // 402
|
|
148
|
+
ForbiddenError, // 403
|
|
149
|
+
NotFoundError, // 404
|
|
150
|
+
MethodNotAllowedError, // 405
|
|
151
|
+
NotAcceptableError, // 406
|
|
152
|
+
RequestTimeoutError, // 408
|
|
153
|
+
ConflictError, // 409
|
|
154
|
+
GoneError, // 410
|
|
155
|
+
LengthRequiredError, // 411
|
|
156
|
+
PreconditionFailedError, // 412
|
|
157
|
+
PayloadTooLargeError, // 413
|
|
158
|
+
UriTooLongError, // 414
|
|
159
|
+
UnsupportedMediaTypeError, // 415
|
|
160
|
+
RangeNotSatisfiableError, // 416
|
|
161
|
+
ExpectationFailedError, // 417
|
|
162
|
+
ImATeapotError, // 418
|
|
163
|
+
UnprocessableEntityError, // 422
|
|
164
|
+
LockedError, // 423
|
|
165
|
+
FailedDependencyError, // 424
|
|
166
|
+
TooEarlyError, // 425
|
|
167
|
+
UpgradeRequiredError, // 426
|
|
168
|
+
PreconditionRequiredError, // 428
|
|
169
|
+
TooManyRequestsError, // 429
|
|
170
|
+
RequestHeaderFieldsTooLargeError, // 431
|
|
171
|
+
UnavailableForLegalReasonsError, // 451
|
|
172
|
+
} from '@nextrush/errors';
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 5xx Server Errors
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import {
|
|
179
|
+
InternalServerError, // 500
|
|
180
|
+
NotImplementedError, // 501
|
|
181
|
+
BadGatewayError, // 502
|
|
182
|
+
ServiceUnavailableError, // 503
|
|
183
|
+
GatewayTimeoutError, // 504
|
|
184
|
+
HttpVersionNotSupportedError, // 505
|
|
185
|
+
VariantAlsoNegotiatesError, // 506
|
|
186
|
+
InsufficientStorageError, // 507
|
|
187
|
+
LoopDetectedError, // 508
|
|
188
|
+
NotExtendedError, // 510
|
|
189
|
+
NetworkAuthRequiredError, // 511
|
|
190
|
+
} from '@nextrush/errors';
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Usage Examples
|
|
194
|
+
|
|
195
|
+
### Basic Usage
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// Simple message
|
|
199
|
+
throw new NotFoundError('User not found');
|
|
200
|
+
|
|
201
|
+
// With error code
|
|
202
|
+
throw new BadRequestError('Invalid email format', {
|
|
203
|
+
code: 'INVALID_EMAIL',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// With additional details
|
|
207
|
+
throw new UnprocessableEntityError('Validation failed', {
|
|
208
|
+
code: 'VALIDATION_ERROR',
|
|
209
|
+
details: {
|
|
210
|
+
email: 'Invalid format',
|
|
211
|
+
age: 'Must be positive',
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### With Error Codes
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
throw new UnauthorizedError('Token expired', {
|
|
220
|
+
code: 'TOKEN_EXPIRED',
|
|
221
|
+
expose: true,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// API response:
|
|
225
|
+
// {
|
|
226
|
+
// "error": "Token expired",
|
|
227
|
+
// "code": "TOKEN_EXPIRED",
|
|
228
|
+
// "status": 401
|
|
229
|
+
// }
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Factory Functions
|
|
233
|
+
|
|
234
|
+
Create errors with less boilerplate:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import {
|
|
238
|
+
badRequest,
|
|
239
|
+
unauthorized,
|
|
240
|
+
forbidden,
|
|
241
|
+
notFound,
|
|
242
|
+
methodNotAllowed,
|
|
243
|
+
conflict,
|
|
244
|
+
unprocessableEntity,
|
|
245
|
+
tooManyRequests,
|
|
246
|
+
internalError,
|
|
247
|
+
serviceUnavailable,
|
|
248
|
+
badGateway,
|
|
249
|
+
gatewayTimeout,
|
|
250
|
+
createError,
|
|
251
|
+
} from '@nextrush/errors';
|
|
252
|
+
|
|
253
|
+
// Quick creation
|
|
254
|
+
throw badRequest('Invalid input');
|
|
255
|
+
throw notFound('Resource not found');
|
|
256
|
+
throw unauthorized('Please login');
|
|
257
|
+
|
|
258
|
+
// With options
|
|
259
|
+
throw tooManyRequests('Rate limit exceeded', {
|
|
260
|
+
code: 'RATE_LIMIT',
|
|
261
|
+
details: { retryAfter: 60 },
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Custom status code
|
|
265
|
+
throw createError(418, "I'm a teapot");
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Validation Errors
|
|
269
|
+
|
|
270
|
+
Specialized errors for input validation:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
import {
|
|
274
|
+
ValidationError,
|
|
275
|
+
RequiredFieldError,
|
|
276
|
+
TypeMismatchError,
|
|
277
|
+
RangeValidationError,
|
|
278
|
+
LengthError,
|
|
279
|
+
PatternError,
|
|
280
|
+
InvalidEmailError,
|
|
281
|
+
InvalidUrlError,
|
|
282
|
+
} from '@nextrush/errors';
|
|
283
|
+
|
|
284
|
+
// Generic validation error with issues
|
|
285
|
+
throw new ValidationError([
|
|
286
|
+
{ path: 'email', message: 'Required', rule: 'required' },
|
|
287
|
+
{ path: 'age', message: 'Must be positive', rule: 'range' },
|
|
288
|
+
]);
|
|
289
|
+
|
|
290
|
+
// Static factory methods
|
|
291
|
+
throw ValidationError.fromField('email', 'Invalid format', 'email');
|
|
292
|
+
throw ValidationError.fromFields({ email: 'Required', age: 'Must be number' });
|
|
293
|
+
|
|
294
|
+
// Specific field errors
|
|
295
|
+
throw new RequiredFieldError('email');
|
|
296
|
+
throw new TypeMismatchError('age', 'number', 'string');
|
|
297
|
+
throw new RangeValidationError('age', 18, 100);
|
|
298
|
+
throw new LengthError('password', 8, 128);
|
|
299
|
+
throw new PatternError('username', '^[a-z0-9]+$');
|
|
300
|
+
throw new InvalidEmailError('email');
|
|
301
|
+
throw new InvalidUrlError('website');
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Error Middleware
|
|
305
|
+
|
|
306
|
+
### errorHandler(options?)
|
|
307
|
+
|
|
308
|
+
Format errors as JSON responses:
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { errorHandler } from '@nextrush/errors';
|
|
312
|
+
|
|
313
|
+
app.use(
|
|
314
|
+
errorHandler({
|
|
315
|
+
// Include stack trace (default: false)
|
|
316
|
+
includeStack: process.env.NODE_ENV !== 'production',
|
|
317
|
+
|
|
318
|
+
// Custom error logger (default: logs 5xx as errors, 4xx as warnings)
|
|
319
|
+
logger: (err, ctx) => {
|
|
320
|
+
myLogger.error(err, { path: ctx.path });
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
// Custom error response transformer
|
|
324
|
+
transform: (err, ctx) => ({
|
|
325
|
+
error: err.message,
|
|
326
|
+
status: ctx.status,
|
|
327
|
+
}),
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### notFoundHandler()
|
|
333
|
+
|
|
334
|
+
Catch-all 404 handler:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
import { notFoundHandler } from '@nextrush/errors';
|
|
338
|
+
|
|
339
|
+
// Add after all routes
|
|
340
|
+
app.use(notFoundHandler());
|
|
341
|
+
|
|
342
|
+
// Custom message
|
|
343
|
+
app.use(notFoundHandler('Route not found'));
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### catchAsync(fn)
|
|
347
|
+
|
|
348
|
+
Wrap async handlers to catch errors:
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import { catchAsync } from '@nextrush/errors';
|
|
352
|
+
|
|
353
|
+
app.get(
|
|
354
|
+
'/users/:id',
|
|
355
|
+
catchAsync(async (ctx) => {
|
|
356
|
+
const user = await db.findUser(ctx.params.id);
|
|
357
|
+
if (!user) throw new NotFoundError('User not found');
|
|
358
|
+
ctx.json(user);
|
|
359
|
+
})
|
|
360
|
+
);
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Error Utilities
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
import { isHttpError, getErrorStatus, getSafeErrorMessage } from '@nextrush/errors';
|
|
367
|
+
|
|
368
|
+
// Check if error is HTTP error
|
|
369
|
+
if (isHttpError(err)) {
|
|
370
|
+
console.log(err.status, err.code);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Get status from any error
|
|
374
|
+
const status = getErrorStatus(err); // Returns 500 for unknown errors
|
|
375
|
+
|
|
376
|
+
// Get safe message (hides internal errors)
|
|
377
|
+
const message = getSafeErrorMessage(err);
|
|
378
|
+
// Returns generic message for 500 errors in production
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Base Classes
|
|
382
|
+
|
|
383
|
+
### HttpError
|
|
384
|
+
|
|
385
|
+
Base class for all HTTP errors:
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
import { HttpError } from '@nextrush/errors';
|
|
389
|
+
|
|
390
|
+
class CustomError extends HttpError {
|
|
391
|
+
constructor(message: string) {
|
|
392
|
+
super(422, message, { code: 'CUSTOM_ERROR' });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### NextRushError
|
|
398
|
+
|
|
399
|
+
Base class for framework errors:
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
import { NextRushError } from '@nextrush/errors';
|
|
403
|
+
|
|
404
|
+
class ConfigError extends NextRushError {
|
|
405
|
+
constructor(message: string) {
|
|
406
|
+
super(message, { code: 'CONFIG_ERROR' });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## API Reference
|
|
412
|
+
|
|
413
|
+
### Exports
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
import {
|
|
417
|
+
// Base classes
|
|
418
|
+
HttpError,
|
|
419
|
+
NextRushError,
|
|
420
|
+
|
|
421
|
+
// 4xx errors
|
|
422
|
+
BadRequestError,
|
|
423
|
+
UnauthorizedError,
|
|
424
|
+
ForbiddenError,
|
|
425
|
+
NotFoundError,
|
|
426
|
+
// ... all 4xx errors
|
|
427
|
+
|
|
428
|
+
// 5xx errors
|
|
429
|
+
InternalServerError,
|
|
430
|
+
ServiceUnavailableError,
|
|
431
|
+
// ... all 5xx errors
|
|
432
|
+
|
|
433
|
+
// Validation errors
|
|
434
|
+
ValidationError,
|
|
435
|
+
RequiredFieldError,
|
|
436
|
+
TypeMismatchError,
|
|
437
|
+
// ... all validation errors
|
|
438
|
+
|
|
439
|
+
// Factory functions
|
|
440
|
+
badRequest,
|
|
441
|
+
unauthorized,
|
|
442
|
+
notFound,
|
|
443
|
+
createError,
|
|
444
|
+
// ... all factory functions
|
|
445
|
+
|
|
446
|
+
// Middleware
|
|
447
|
+
errorHandler,
|
|
448
|
+
notFoundHandler,
|
|
449
|
+
catchAsync,
|
|
450
|
+
|
|
451
|
+
// Utilities
|
|
452
|
+
isHttpError,
|
|
453
|
+
getErrorStatus,
|
|
454
|
+
getSafeErrorMessage,
|
|
455
|
+
} from '@nextrush/errors';
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Types
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
import type {
|
|
462
|
+
HttpErrorOptions,
|
|
463
|
+
ValidationIssue,
|
|
464
|
+
ErrorContext,
|
|
465
|
+
ErrorHandlerOptions,
|
|
466
|
+
ErrorMiddleware,
|
|
467
|
+
} from '@nextrush/errors';
|
|
468
|
+
|
|
469
|
+
interface HttpErrorOptions {
|
|
470
|
+
code?: string;
|
|
471
|
+
expose?: boolean;
|
|
472
|
+
details?: Record<string, unknown>;
|
|
473
|
+
cause?: unknown;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
interface ValidationIssue {
|
|
477
|
+
path: string;
|
|
478
|
+
message: string;
|
|
479
|
+
rule?: string;
|
|
480
|
+
expected?: unknown;
|
|
481
|
+
received?: unknown;
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
````
|
|
486
|
+
|
|
487
|
+
## Common Mistakes
|
|
488
|
+
|
|
489
|
+
### Mistake 1: Using Generic Errors for Specific Cases
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
// ❌ Don't do this
|
|
493
|
+
throw new Error('User not found');
|
|
494
|
+
// → Returns 500 Internal Server Error, no error code
|
|
495
|
+
|
|
496
|
+
// ✅ Do this instead
|
|
497
|
+
throw new NotFoundError('User not found');
|
|
498
|
+
// → Returns 404 Not Found with NOT_FOUND code
|
|
499
|
+
````
|
|
500
|
+
|
|
501
|
+
**Why it's wrong:** Generic JavaScript `Error` becomes 500 Internal Server Error. The client can't distinguish between "not found" and "server crash".
|
|
502
|
+
|
|
503
|
+
### Mistake 2: Exposing Internal Implementation Details
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
// ❌ Don't do this
|
|
507
|
+
throw new InternalServerError('PostgreSQL connection timeout on pool "primary"', {
|
|
508
|
+
expose: true, // Leaks infrastructure details!
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// ✅ Do this instead
|
|
512
|
+
throw new InternalServerError('Database temporarily unavailable');
|
|
513
|
+
// expose defaults to false for 5xx errors
|
|
514
|
+
// Full error logged server-side for debugging
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
**Why it's wrong:** Exposing database names, connection pools, or internal service names helps attackers understand your infrastructure.
|
|
518
|
+
|
|
519
|
+
### Mistake 3: Forgetting Error Middleware
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
// ❌ Errors won't be formatted
|
|
523
|
+
const app = createApp();
|
|
524
|
+
|
|
525
|
+
app.get('/users', (ctx) => {
|
|
526
|
+
throw new NotFoundError('User not found'); // Crashes or returns HTML error page!
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// ✅ Add errorHandler BEFORE routes
|
|
530
|
+
const app = createApp();
|
|
531
|
+
app.use(errorHandler()); // This catches and formats errors
|
|
532
|
+
|
|
533
|
+
app.get('/users', (ctx) => {
|
|
534
|
+
throw new NotFoundError('User not found'); // Returns proper JSON
|
|
535
|
+
});
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Mistake 4: Using Errors for Control Flow
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
// ❌ Don't use errors for expected business logic
|
|
542
|
+
async function getUser(id: string): Promise<User> {
|
|
543
|
+
const user = await db.findUser(id);
|
|
544
|
+
if (!user) throw new NotFoundError(); // Too expensive for expected case
|
|
545
|
+
return user;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ✅ Use nullable returns for expected cases
|
|
549
|
+
async function getUser(id: string): Promise<User | null> {
|
|
550
|
+
return await db.findUser(id);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ✅ Throw errors at the HTTP boundary
|
|
554
|
+
app.get('/users/:id', async (ctx) => {
|
|
555
|
+
const user = await getUser(ctx.params.id);
|
|
556
|
+
if (!user) throw new NotFoundError('User not found');
|
|
557
|
+
ctx.json(user);
|
|
558
|
+
});
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
**Why it's wrong:** Throwing errors for control flow is expensive (stack trace construction) and makes code harder to reason about.
|
|
562
|
+
|
|
563
|
+
### Mistake 5: Missing Error Codes for API Clients
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
// ❌ Clients can't handle errors programmatically
|
|
567
|
+
throw new BadRequestError('Invalid email format');
|
|
568
|
+
// Response: {"message": "Invalid email format"} // No code!
|
|
569
|
+
|
|
570
|
+
// ✅ Include error codes
|
|
571
|
+
throw new BadRequestError('Invalid email format', {
|
|
572
|
+
code: 'INVALID_EMAIL',
|
|
573
|
+
});
|
|
574
|
+
// Response: {"message": "Invalid email format", "code": "INVALID_EMAIL"}
|
|
575
|
+
|
|
576
|
+
// Client can now:
|
|
577
|
+
if (error.code === 'INVALID_EMAIL') {
|
|
578
|
+
// Show email field error
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
## When NOT to Use
|
|
583
|
+
|
|
584
|
+
### Don't Use for Validation in Reusable Libraries
|
|
585
|
+
|
|
586
|
+
If you're building a reusable library (not an HTTP handler), return validation results instead of throwing:
|
|
587
|
+
|
|
588
|
+
```typescript
|
|
589
|
+
// ❌ Don't throw in library code
|
|
590
|
+
export function validateEmail(email: string): string {
|
|
591
|
+
if (!isValid(email)) throw new InvalidEmailError(); // Caller can't control behavior
|
|
592
|
+
return email;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// ✅ Return validation result
|
|
596
|
+
export function validateEmail(email: string): { valid: boolean; error?: string } {
|
|
597
|
+
return isValid(email) ? { valid: true } : { valid: false, error: 'Invalid email format' };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ✅ Throw at the HTTP boundary
|
|
601
|
+
app.post('/users', (ctx) => {
|
|
602
|
+
const result = validateEmail(ctx.body.email);
|
|
603
|
+
if (!result.valid) {
|
|
604
|
+
throw new BadRequestError(result.error!, { code: 'INVALID_EMAIL' });
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
**Why:** Libraries should be composable. Throwing errors forces a specific error handling strategy on consumers.
|
|
610
|
+
|
|
611
|
+
### Don't Use for Expected Empty Results
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
// ❌ Don't throw for queries that might return nothing
|
|
615
|
+
async function searchUsers(query: string): Promise<User[]> {
|
|
616
|
+
const users = await db.search(query);
|
|
617
|
+
if (users.length === 0) throw new NotFoundError(); // Expected case!
|
|
618
|
+
return users;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ✅ Return empty arrays for "no results"
|
|
622
|
+
async function searchUsers(query: string): Promise<User[]> {
|
|
623
|
+
return await db.search(query); // Empty array is valid
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ✅ Only throw when the resource *should* exist
|
|
627
|
+
app.get('/users/:id', async (ctx) => {
|
|
628
|
+
const user = await db.getById(ctx.params.id);
|
|
629
|
+
if (!user) throw new NotFoundError(); // Specific ID should exist
|
|
630
|
+
ctx.json(user);
|
|
631
|
+
});
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### Don't Use for Non-HTTP Contexts
|
|
635
|
+
|
|
636
|
+
```typescript
|
|
637
|
+
// ❌ Don't use HTTP errors in background jobs
|
|
638
|
+
async function processQueue() {
|
|
639
|
+
const job = await queue.pop();
|
|
640
|
+
if (!job) throw new NotFoundError(); // Wrong abstraction!
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ✅ Use domain-specific errors or return values
|
|
644
|
+
async function processQueue() {
|
|
645
|
+
const job = await queue.pop();
|
|
646
|
+
if (!job) return { processed: false, reason: 'queue_empty' };
|
|
647
|
+
// Process job...
|
|
648
|
+
return { processed: true };
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
## Runtime Compatibility
|
|
653
|
+
|
|
654
|
+
Works on all NextRush-supported runtimes:
|
|
655
|
+
|
|
656
|
+
| Runtime | Supported | Notes |
|
|
657
|
+
| ------------------- | --------- | ------------ |
|
|
658
|
+
| Node.js 20+ | ✅ | Full support |
|
|
659
|
+
| Bun 1.0+ | ✅ | Full support |
|
|
660
|
+
| Deno 2.0+ | ✅ | Full support |
|
|
661
|
+
| Cloudflare Workers | ✅ | Full support |
|
|
662
|
+
| Vercel Edge Runtime | ✅ | Full support |
|
|
663
|
+
|
|
664
|
+
**Zero external dependencies.** Uses only standard JavaScript `Error` APIs and NextRush types.
|
|
665
|
+
|
|
666
|
+
## Best Practices
|
|
667
|
+
|
|
668
|
+
1. **Use specific errors**: `NotFoundError` over generic `HttpError`
|
|
669
|
+
2. **Include error codes**: Help API consumers handle errors programmatically
|
|
670
|
+
3. **Don't expose internals**: Keep `expose: false` for 5xx errors (default)
|
|
671
|
+
4. **Add error middleware first**: Before all routes
|
|
672
|
+
5. **Validate early**: Throw validation errors at the start of handlers
|
|
673
|
+
6. **Return nulls for expected "not found"**: Throw errors only at HTTP boundaries
|
|
674
|
+
|
|
675
|
+
## License
|
|
676
|
+
|
|
677
|
+
MIT
|
|
678
|
+
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
```
|