@nextrush/errors 3.0.6 → 4.0.0-beta.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/README.md +321 -522
- package/dist/index.d.ts +88 -38
- package/dist/index.js +185 -16
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/audit-fixes.test.ts +181 -0
- package/src/__tests__/base.test.ts +2 -2
- package/src/__tests__/factory.test.ts +9 -2
- package/src/__tests__/middleware.test.ts +38 -33
- package/src/__tests__/public-surface.test.ts +118 -0
- package/src/base.ts +148 -3
- package/src/codes.ts +77 -0
- package/src/factory.ts +58 -1
- package/src/http-errors.ts +6 -0
- package/src/index.ts +4 -8
- package/src/middleware.ts +9 -43
- package/src/validation.ts +2 -1
package/README.md
CHANGED
|
@@ -1,84 +1,107 @@
|
|
|
1
1
|
# @nextrush/errors
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> A typed `HttpError` hierarchy and error-handling middleware that turn thrown errors into consistent, client-safe JSON responses — for NextRush application and middleware authors.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@nextrush/errors)
|
|
6
|
+
[](https://www.npmjs.com/package/@nextrush/errors)
|
|
7
|
+
[](https://bundlephobia.com/package/@nextrush/errors)
|
|
8
|
+
[](https://www.npmjs.com/package/@nextrush/errors)
|
|
9
|
+
[](https://nodejs.org/api/esm.html)
|
|
10
|
+
[](https://github.com/0xTanzim/nextRush/blob/main/LICENSE)
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
| | |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| **Purpose** | Typed HTTP errors + error-handling middleware for NextRush — throw an error, get a consistent JSON response |
|
|
15
|
+
| **Package type** | Core |
|
|
16
|
+
| **Status** | Stable ✅ |
|
|
17
|
+
| **Included in `nextrush`?** | ✅ Yes — the common error classes, `errorHandler`, `ValidationError`, `createError`, `ERROR_CODES` are re-exported. Install directly for the full catalog (every 4xx/5xx class, every factory helper, every validation subclass). |
|
|
18
|
+
| **Support tier** | Public — core (stable, semver-guarded) — see [ADR-0005](https://github.com/0xTanzim/nextRush/blob/main/docs/adr/ADR-0005-package-tiers-sealed-surface-deprecation.md) |
|
|
19
|
+
| **Maintenance** | Active |
|
|
20
|
+
| **Runtime** | Universal — Node · Bun · Deno · Edge |
|
|
21
|
+
| **Requires** | Node `>=22` · ESM-only · TypeScript `>=5.x` |
|
|
22
|
+
| **Introduced** | `v3.0.0` |
|
|
8
23
|
|
|
9
|
-
|
|
24
|
+
## Highlights
|
|
10
25
|
|
|
11
|
-
|
|
26
|
+
- ✅ **No third-party dependencies** — depends only on `@nextrush/types` (types, erased at build)
|
|
27
|
+
- ✅ **ESM-only**, tree-shakable, side-effect-free — a thrown error pulls in only its own class
|
|
28
|
+
- ✅ **Fully typed** — strict TypeScript, zero `any`; every error carries `status`, `code`, and `expose`
|
|
29
|
+
- 🛡️ **Client-safe by default** — 5xx messages are hidden unless you opt in; internal detail never leaks
|
|
12
30
|
|
|
13
|
-
|
|
31
|
+
<details>
|
|
32
|
+
<summary><strong>Table of contents</strong></summary>
|
|
14
33
|
|
|
15
|
-
|
|
34
|
+
[The problem](#the-problem) · [When to use](#when-to-use) · [Installation](#installation) · [Quick start](#quick-start) · [Capabilities](#capabilities) · [Mental model](#mental-model) · [Common tasks](#common-tasks) · [API overview](#api-overview) · [Options](#options) · [Compatibility](#compatibility) · [Troubleshooting](#troubleshooting) · [FAQ](#faq) · [Package relationships](#package-relationships) · [Architecture](#architecture) · [Resources](#resources)
|
|
16
35
|
|
|
17
|
-
|
|
36
|
+
</details>
|
|
18
37
|
|
|
19
|
-
|
|
38
|
+
---
|
|
20
39
|
|
|
21
|
-
|
|
40
|
+
## The problem
|
|
22
41
|
|
|
23
|
-
|
|
24
|
-
2. **Human message** - Clear explanation for developers/users
|
|
25
|
-
3. **Machine code** - Stable identifier for programmatic handling
|
|
42
|
+
Error handling is where consistency quietly erodes. One handler returns `{ error: "..." }`, the next returns `{ message: "..." }`, and a forgotten `try/catch` leaks a database stack trace straight to the client. Each handler re-decides the status code, the response shape, and what is safe to expose — and each one gets it slightly differently.
|
|
26
43
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
```ts
|
|
45
|
+
// TODAY, without a typed error layer — the shape drifts per handler,
|
|
46
|
+
// and the internal message goes straight to the client:
|
|
47
|
+
app.get('/users/:id', async (ctx) => {
|
|
48
|
+
const user = await db.findUser(ctx.params.id);
|
|
49
|
+
if (!user) {
|
|
50
|
+
ctx.status = 404;
|
|
51
|
+
return ctx.json({ error: 'not found' }); // one shape here…
|
|
52
|
+
}
|
|
53
|
+
// if db.findUser throws, the raw message ("ECONNREFUSED 10.0.0.5:5432")
|
|
54
|
+
// reaches the client unless every handler remembers to catch it.
|
|
55
|
+
});
|
|
39
56
|
```
|
|
40
57
|
|
|
41
|
-
|
|
58
|
+
As the app grows, API consumers can't handle errors programmatically (no stable codes), and each new endpoint is another chance to leak infrastructure detail. `@nextrush/errors` makes the error *itself* carry its status, machine code, and exposure rule — so the response shape is decided once, not per handler.
|
|
42
59
|
|
|
43
|
-
|
|
44
|
-
- **Code:** `NOT_FOUND`
|
|
45
|
-
- **Message:** Custom message you provide
|
|
46
|
-
- **Format:** Consistent JSON structure
|
|
60
|
+
## When to use
|
|
47
61
|
|
|
48
|
-
|
|
62
|
+
`@nextrush/errors` is the error layer built into NextRush. `throw new NotFoundError(...)` from any handler, add `errorHandler()` to the middleware chain, and every error serializes the same way.
|
|
49
63
|
|
|
50
|
-
|
|
64
|
+
**Use `@nextrush/errors` if:**
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
66
|
+
- ✓ You're building a NextRush app or middleware and want thrown errors to become consistent JSON automatically
|
|
67
|
+
- ✓ You need stable machine-readable `code`s (`NOT_FOUND`, `RATE_LIMIT`) that API clients can branch on
|
|
68
|
+
- ✓ You want 5xx internals hidden from clients by default, while still logging the full error server-side
|
|
69
|
+
- ✓ You need structured field-level validation errors (`ValidationError`) or to rebuild a typed error across a service boundary (`fromJSON`)
|
|
56
70
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
```
|
|
71
|
+
**Reach for something else if:**
|
|
72
|
+
|
|
73
|
+
- ✗ You're writing a reusable, transport-agnostic library — return a result value instead of throwing HTTP errors; throw at the HTTP boundary only
|
|
74
|
+
- ✗ You want request-body size limits or content negotiation — those live in [`@nextrush/body-parser`](../middleware/body-parser) and the handler, not here
|
|
62
75
|
|
|
63
|
-
|
|
76
|
+
---
|
|
64
77
|
|
|
65
78
|
## Installation
|
|
66
79
|
|
|
67
80
|
```bash
|
|
68
81
|
pnpm add @nextrush/errors
|
|
82
|
+
# npm i @nextrush/errors · yarn add @nextrush/errors · bun add @nextrush/errors
|
|
69
83
|
```
|
|
70
84
|
|
|
71
|
-
|
|
85
|
+
> [!NOTE]
|
|
86
|
+
> Already using `nextrush`? The common error classes plus `errorHandler`, `notFoundHandler`,
|
|
87
|
+
> `createError`, `ValidationError`, `ERROR_CODES`, and `codeForStatus` are re-exported from the
|
|
88
|
+
> meta package — `import { NotFoundError, errorHandler } from 'nextrush'` works without installing
|
|
89
|
+
> this directly. Install `@nextrush/errors` when you need the full catalog or want to depend on it
|
|
90
|
+
> explicitly.
|
|
72
91
|
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
## Quick start
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { createApp, listen } from 'nextrush';
|
|
75
96
|
import { errorHandler, NotFoundError, BadRequestError } from '@nextrush/errors';
|
|
76
97
|
|
|
77
98
|
const app = createApp();
|
|
78
99
|
|
|
79
|
-
//
|
|
100
|
+
// Register the handler BEFORE your routes — it wraps the chain in a try/catch.
|
|
80
101
|
app.use(errorHandler());
|
|
81
102
|
|
|
103
|
+
const users = new Map([['1', { id: '1', name: 'Ada' }]]);
|
|
104
|
+
|
|
82
105
|
app.get('/users/:id', (ctx) => {
|
|
83
106
|
const user = users.get(ctx.params.id);
|
|
84
107
|
if (!user) {
|
|
@@ -88,594 +111,370 @@ app.get('/users/:id', (ctx) => {
|
|
|
88
111
|
});
|
|
89
112
|
|
|
90
113
|
app.post('/users', (ctx) => {
|
|
91
|
-
|
|
92
|
-
|
|
114
|
+
const { email } = (ctx.body ?? {}) as { email?: string };
|
|
115
|
+
if (!email) {
|
|
116
|
+
throw new BadRequestError('Email is required', { code: 'EMAIL_REQUIRED' });
|
|
93
117
|
}
|
|
94
|
-
|
|
95
|
-
ctx.json({
|
|
118
|
+
ctx.status = 201;
|
|
119
|
+
ctx.json({ ok: true });
|
|
96
120
|
});
|
|
97
121
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
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
|
-
// }
|
|
122
|
+
listen(app, 8080);
|
|
123
|
+
|
|
124
|
+
// GET /users/999 → 404
|
|
125
|
+
// { "error": "NotFoundError", "message": "User not found", "code": "NOT_FOUND", "status": 404 }
|
|
126
|
+
//
|
|
127
|
+
// POST /users (no email) → 400
|
|
128
|
+
// { "error": "BadRequestError", "message": "Email is required", "code": "EMAIL_REQUIRED", "status": 400 }
|
|
115
129
|
```
|
|
116
130
|
|
|
117
|
-
|
|
131
|
+
You never write the response shape. You declare an error state by throwing a typed error, and `errorHandler()` catches it, sets the status, and serializes it through the error's own `toJSON()`.
|
|
118
132
|
|
|
119
|
-
|
|
133
|
+
## Capabilities
|
|
120
134
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
5. **Logs appropriately** - 5xx logged as errors, 4xx as warnings
|
|
126
|
-
6. **Preserves stack traces** - Full debugging in development, hidden in production
|
|
135
|
+
**Error types**
|
|
136
|
+
- **Full `HttpError` catalog** — 28 client (4xx) and 11 server (5xx) classes, each with the correct status and canonical `code`
|
|
137
|
+
- **`ValidationError` family** — structured, multi-issue validation errors with field-level helpers
|
|
138
|
+
- **Custom errors** — extend `HttpError` or `NextRushError` to add your own typed errors
|
|
127
139
|
|
|
128
|
-
|
|
140
|
+
**Safety**
|
|
141
|
+
- **`expose` privacy boundary** — 4xx messages are shown, 5xx messages hidden by default; internal detail and stack traces stay server-side
|
|
142
|
+
- **Bounded `cause` serialization** — nested `cause` chains are walked to a fixed depth with a cycle guard, and only on exposed errors
|
|
143
|
+
- **Immutable details** — `details` and validation `issues` are frozen at construction, so an error can't be mutated after it's thrown
|
|
129
144
|
|
|
130
|
-
|
|
145
|
+
**Integration**
|
|
146
|
+
- **`errorHandler()` middleware** — one Koa-style middleware catches, logs, and serializes every thrown error
|
|
147
|
+
- **Stable machine codes** — a central `ERROR_CODES` registry maps every status to one canonical code
|
|
148
|
+
- **Cross-service transport** — `toJSON()` / `fromJSON()` round-trips a typed error across an HTTP boundary
|
|
131
149
|
|
|
132
|
-
|
|
133
|
-
- **
|
|
134
|
-
- **
|
|
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
|
|
150
|
+
**Developer experience**
|
|
151
|
+
- **Factory helpers** — `notFound()`, `badRequest()`, `createError(status)` for terse construction
|
|
152
|
+
- **Fully typed** — strict TypeScript, zero `any`; contracts shared via `@nextrush/types`
|
|
138
153
|
|
|
139
|
-
##
|
|
154
|
+
## Mental model
|
|
140
155
|
|
|
141
|
-
|
|
156
|
+
An error is an **API response object**, not a crash. Throwing it declares a status, a machine code, and whether its message is safe to show — the framework does the rest.
|
|
142
157
|
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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';
|
|
158
|
+
```text
|
|
159
|
+
throw new XError(msg)
|
|
160
|
+
│ status · code · expose · details
|
|
161
|
+
▼
|
|
162
|
+
errorHandler() catch ──▶ log (5xx→error, 4xx→warn) ──▶ err.toJSON() ──▶ ctx.json(body)
|
|
163
|
+
│
|
|
164
|
+
expose === false ───────┴──▶ message becomes "Internal Server Error"
|
|
173
165
|
```
|
|
174
166
|
|
|
175
|
-
|
|
167
|
+
**Rule:** the `expose` flag is the privacy boundary — `true` for 4xx (client's fault, safe to explain), `false` for 5xx (your fault, don't leak). Override it deliberately, never by accident.
|
|
176
168
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
```
|
|
169
|
+
> [!TIP]
|
|
170
|
+
> The full error hierarchy, the throw→response sequence, and the state lifecycle (with Mermaid
|
|
171
|
+
> diagrams) are in [`ARCHITECTURE.md`](./ARCHITECTURE.md).
|
|
192
172
|
|
|
193
|
-
|
|
173
|
+
---
|
|
194
174
|
|
|
195
|
-
|
|
175
|
+
## Common tasks
|
|
196
176
|
|
|
197
|
-
|
|
198
|
-
// Simple message
|
|
199
|
-
throw new NotFoundError('User not found');
|
|
177
|
+
### Throw a typed HTTP error
|
|
200
178
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
code: 'INVALID_EMAIL',
|
|
204
|
-
});
|
|
179
|
+
```ts
|
|
180
|
+
import { NotFoundError, ForbiddenError } from '@nextrush/errors';
|
|
205
181
|
|
|
206
|
-
//
|
|
207
|
-
throw new
|
|
208
|
-
code: 'VALIDATION_ERROR',
|
|
209
|
-
details: {
|
|
210
|
-
email: 'Invalid format',
|
|
211
|
-
age: 'Must be positive',
|
|
212
|
-
},
|
|
213
|
-
});
|
|
182
|
+
throw new NotFoundError('User not found'); // 404, code NOT_FOUND, expose true
|
|
183
|
+
throw new ForbiddenError(); // 403, default message "Forbidden"
|
|
214
184
|
```
|
|
215
185
|
|
|
216
|
-
|
|
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
|
-
```
|
|
186
|
+
Every class defaults its message, status, and `code` — pass a message to override the default, and options to add more.
|
|
231
187
|
|
|
232
|
-
|
|
188
|
+
### Attach a machine code and details for API clients
|
|
233
189
|
|
|
234
|
-
|
|
190
|
+
```ts
|
|
191
|
+
import { UnprocessableEntityError } from '@nextrush/errors';
|
|
235
192
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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 },
|
|
193
|
+
throw new UnprocessableEntityError('Validation failed', {
|
|
194
|
+
code: 'VALIDATION_ERROR',
|
|
195
|
+
details: { email: 'Invalid format', age: 'Must be positive' },
|
|
262
196
|
});
|
|
263
|
-
|
|
264
|
-
// Custom status code
|
|
265
|
-
throw createError(418, "I'm a teapot");
|
|
197
|
+
// → 422 { error, message, code: 'VALIDATION_ERROR', status: 422, details: { … } }
|
|
266
198
|
```
|
|
267
199
|
|
|
268
|
-
|
|
200
|
+
`details` is surfaced in the JSON only when the error is exposed (all 4xx are, by default), and it's frozen so the error owns an immutable snapshot.
|
|
269
201
|
|
|
270
|
-
|
|
202
|
+
### Report field-level validation failures
|
|
271
203
|
|
|
272
|
-
```
|
|
273
|
-
import {
|
|
274
|
-
ValidationError,
|
|
275
|
-
RequiredFieldError,
|
|
276
|
-
TypeMismatchError,
|
|
277
|
-
RangeValidationError,
|
|
278
|
-
LengthError,
|
|
279
|
-
PatternError,
|
|
280
|
-
InvalidEmailError,
|
|
281
|
-
InvalidUrlError,
|
|
282
|
-
} from '@nextrush/errors';
|
|
204
|
+
```ts
|
|
205
|
+
import { ValidationError, RequiredFieldError } from '@nextrush/errors';
|
|
283
206
|
|
|
284
|
-
//
|
|
207
|
+
// Multiple issues at once
|
|
285
208
|
throw new ValidationError([
|
|
286
209
|
{ path: 'email', message: 'Required', rule: 'required' },
|
|
287
210
|
{ path: 'age', message: 'Must be positive', rule: 'range' },
|
|
288
211
|
]);
|
|
289
212
|
|
|
290
|
-
//
|
|
213
|
+
// Or the terse factory forms
|
|
291
214
|
throw ValidationError.fromField('email', 'Invalid format', 'email');
|
|
292
|
-
throw ValidationError.fromFields({ email: 'Required', age: 'Must be number' });
|
|
293
|
-
|
|
294
|
-
// Specific field errors
|
|
215
|
+
throw ValidationError.fromFields({ email: 'Required', age: 'Must be a number' });
|
|
295
216
|
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
217
|
```
|
|
345
218
|
|
|
346
|
-
|
|
219
|
+
`ValidationError` serializes an `issues` array and strips each issue's `received` value, so a rejected password or token is never echoed back.
|
|
347
220
|
|
|
348
|
-
|
|
221
|
+
### Install the error handler and a 404 fallback
|
|
349
222
|
|
|
350
|
-
```
|
|
351
|
-
import {
|
|
223
|
+
```ts
|
|
224
|
+
import { errorHandler, notFoundHandler } from '@nextrush/errors';
|
|
352
225
|
|
|
353
|
-
app.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const user = await db.findUser(ctx.params.id);
|
|
357
|
-
if (!user) throw new NotFoundError('User not found');
|
|
358
|
-
ctx.json(user);
|
|
359
|
-
})
|
|
360
|
-
);
|
|
226
|
+
app.use(errorHandler({ includeStack: process.env.NODE_ENV !== 'production' })); // first
|
|
227
|
+
// … your routes …
|
|
228
|
+
app.use(notFoundHandler('Route not found')); // last — turns an unhandled 404 into JSON
|
|
361
229
|
```
|
|
362
230
|
|
|
363
|
-
|
|
231
|
+
`errorHandler()` wraps the rest of the chain in a `try/catch`; `notFoundHandler()` responds only when nothing else did and the status is `404`.
|
|
364
232
|
|
|
365
|
-
|
|
366
|
-
import { isHttpError, getErrorStatus, getSafeErrorMessage } from '@nextrush/errors';
|
|
233
|
+
### Create an error by status, or rebuild one across a service boundary
|
|
367
234
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
console.log(err.status, err.code);
|
|
371
|
-
}
|
|
235
|
+
```ts
|
|
236
|
+
import { createError, codeForStatus, HttpError } from '@nextrush/errors';
|
|
372
237
|
|
|
373
|
-
//
|
|
374
|
-
|
|
238
|
+
throw createError(413, 'Upload too large'); // → PayloadTooLargeError, code PAYLOAD_TOO_LARGE
|
|
239
|
+
codeForStatus(429); // 'TOO_MANY_REQUESTS'
|
|
375
240
|
|
|
376
|
-
//
|
|
377
|
-
const
|
|
378
|
-
// Returns generic message for 500 errors in production
|
|
241
|
+
// Downstream service: rebuild a typed error from the JSON it received
|
|
242
|
+
const restored = HttpError.fromJSON(payload); // instanceof HttpError works again
|
|
379
243
|
```
|
|
380
244
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
### HttpError
|
|
245
|
+
`createError(status)` returns the correctly-typed class for any status that has one (falling back to a bare `HttpError` otherwise); `fromJSON()` reverses `toJSON()`.
|
|
384
246
|
|
|
385
|
-
|
|
247
|
+
## API overview
|
|
386
248
|
|
|
387
|
-
|
|
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
|
-
```
|
|
249
|
+
The sealed public surface (ADR-0005), grouped by role.
|
|
396
250
|
|
|
397
|
-
|
|
251
|
+
| Export | Signature | Since | Stability | Description |
|
|
252
|
+
| ------ | --------- | ----- | --------- | ----------- |
|
|
253
|
+
| `NextRushError` | `class (message, options?)` | `3.0.0` | Stable ✅ | Base error — owns `status`, `code`, `expose`, `details`, `cause`, `toJSON()`, `toResponse()`, `fromJSON()`. |
|
|
254
|
+
| `HttpError` | `class (status, message?, options?)` | `3.0.0` | Stable ✅ | Base for all HTTP status errors; resolves `code` via the registry and `expose` from the status. |
|
|
255
|
+
| `ValidationError` | `class (issues, message?)` | `3.0.0` | Stable ✅ | Multi-issue validation error (extends `NextRushError`, **not** `HttpError`); status `400`. |
|
|
256
|
+
| `createError` | `(status, message?, options?) => HttpError` | `3.0.0` | Stable ✅ | Build the correctly-typed error for a status code. |
|
|
257
|
+
| `errorHandler` | `(options?: ErrorHandlerOptions) => Middleware` | `3.0.0` | Stable ✅ | Catch, log, and serialize thrown errors as JSON. |
|
|
258
|
+
| `notFoundHandler` | `(message?: string) => Middleware` | `3.0.0` | Stable ✅ | JSON 404 fallback for unhandled requests. |
|
|
259
|
+
| `isHttpError` | `(error) => error is HttpError` | `3.0.0` | Stable ✅ | Type guard for `HttpError` (note: `false` for `ValidationError`). |
|
|
260
|
+
| `getErrorStatus` | `(error) => number` | `3.0.0` | Stable ✅ | Status from any error (any `NextRushError`, duck-typed `status`, else `500`). |
|
|
261
|
+
| `getSafeErrorMessage` | `(error) => string` | `3.0.0` | Stable ✅ | Message if the error is exposed, else `'Internal Server Error'`. |
|
|
262
|
+
| `getHttpStatusMessage` | `(status) => string` | `3.0.0` | Stable ✅ | Canonical reason phrase for a status. |
|
|
263
|
+
| `ERROR_CODES` | `Readonly<Record<number, string>>` | `3.1.0` | Stable ✅ | Frozen status→canonical-code registry. |
|
|
264
|
+
| `codeForStatus` | `(status) => string` | `3.1.0` | Stable ✅ | Canonical code for a status (`HTTP_<status>` if none). |
|
|
265
|
+
| `GENERIC_ERROR_CODE` · `VALIDATION_ERROR_CODE` | `string` | `3.1.0` | Stable ✅ | `'INTERNAL_ERROR'` · `'VALIDATION_ERROR'`. |
|
|
266
|
+
| `type HttpErrorOptions` · `ValidationIssue` · `ErrorHandlerOptions` | — | `3.0.0` | Stable ✅ | Public option/data contracts. |
|
|
398
267
|
|
|
399
|
-
|
|
268
|
+
### HTTP error classes
|
|
400
269
|
|
|
401
|
-
|
|
402
|
-
import { NextRushError } from '@nextrush/errors';
|
|
270
|
+
Each class sets its status and canonical `code`; the message defaults to the status reason phrase.
|
|
403
271
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
272
|
+
```ts
|
|
273
|
+
import {
|
|
274
|
+
// 4xx client errors — expose: true by default
|
|
275
|
+
BadRequestError, // 400 BAD_REQUEST
|
|
276
|
+
UnauthorizedError, // 401 UNAUTHORIZED
|
|
277
|
+
PaymentRequiredError, // 402 PAYMENT_REQUIRED
|
|
278
|
+
ForbiddenError, // 403 FORBIDDEN
|
|
279
|
+
NotFoundError, // 404 NOT_FOUND
|
|
280
|
+
MethodNotAllowedError, // 405 METHOD_NOT_ALLOWED (constructor: allowedMethods first)
|
|
281
|
+
NotAcceptableError, // 406 NOT_ACCEPTABLE
|
|
282
|
+
ProxyAuthRequiredError, // 407 PROXY_AUTH_REQUIRED
|
|
283
|
+
RequestTimeoutError, // 408 REQUEST_TIMEOUT
|
|
284
|
+
ConflictError, // 409 CONFLICT
|
|
285
|
+
GoneError, // 410 GONE
|
|
286
|
+
LengthRequiredError, // 411 LENGTH_REQUIRED
|
|
287
|
+
PreconditionFailedError, // 412 PRECONDITION_FAILED
|
|
288
|
+
PayloadTooLargeError, // 413 PAYLOAD_TOO_LARGE
|
|
289
|
+
UriTooLongError, // 414 URI_TOO_LONG
|
|
290
|
+
UnsupportedMediaTypeError, // 415 UNSUPPORTED_MEDIA_TYPE
|
|
291
|
+
RangeNotSatisfiableError, // 416 RANGE_NOT_SATISFIABLE
|
|
292
|
+
ExpectationFailedError, // 417 EXPECTATION_FAILED
|
|
293
|
+
ImATeapotError, // 418 IM_A_TEAPOT
|
|
294
|
+
UnprocessableEntityError, // 422 UNPROCESSABLE_ENTITY
|
|
295
|
+
LockedError, // 423 LOCKED
|
|
296
|
+
FailedDependencyError, // 424 FAILED_DEPENDENCY
|
|
297
|
+
TooEarlyError, // 425 TOO_EARLY
|
|
298
|
+
UpgradeRequiredError, // 426 UPGRADE_REQUIRED
|
|
299
|
+
PreconditionRequiredError, // 428 PRECONDITION_REQUIRED
|
|
300
|
+
TooManyRequestsError, // 429 TOO_MANY_REQUESTS (options.retryAfter)
|
|
301
|
+
RequestHeaderFieldsTooLargeError, // 431 REQUEST_HEADER_FIELDS_TOO_LARGE
|
|
302
|
+
UnavailableForLegalReasonsError, // 451 UNAVAILABLE_FOR_LEGAL_REASONS
|
|
303
|
+
|
|
304
|
+
// 5xx server errors — expose: false by default
|
|
305
|
+
InternalServerError, // 500 INTERNAL_SERVER_ERROR
|
|
306
|
+
NotImplementedError, // 501 NOT_IMPLEMENTED
|
|
307
|
+
BadGatewayError, // 502 BAD_GATEWAY
|
|
308
|
+
ServiceUnavailableError, // 503 SERVICE_UNAVAILABLE (options.retryAfter)
|
|
309
|
+
GatewayTimeoutError, // 504 GATEWAY_TIMEOUT
|
|
310
|
+
HttpVersionNotSupportedError, // 505 HTTP_VERSION_NOT_SUPPORTED
|
|
311
|
+
VariantAlsoNegotiatesError, // 506 VARIANT_ALSO_NEGOTIATES
|
|
312
|
+
InsufficientStorageError, // 507 INSUFFICIENT_STORAGE
|
|
313
|
+
LoopDetectedError, // 508 LOOP_DETECTED
|
|
314
|
+
NotExtendedError, // 510 NOT_EXTENDED
|
|
315
|
+
NetworkAuthRequiredError, // 511 NETWORK_AUTH_REQUIRED
|
|
316
|
+
} from '@nextrush/errors';
|
|
409
317
|
```
|
|
410
318
|
|
|
411
|
-
|
|
319
|
+
### Validation error classes
|
|
412
320
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
```typescript
|
|
321
|
+
```ts
|
|
416
322
|
import {
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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,
|
|
323
|
+
ValidationError, // base — issues[], fromField(), fromFields(), hasErrorFor(), toFlatObject()
|
|
324
|
+
RequiredFieldError, // "<field> is required" rule: required
|
|
325
|
+
TypeMismatchError, // expected vs received type rule: type
|
|
326
|
+
RangeValidationError, // numeric min/max rule: range
|
|
327
|
+
LengthError, // string length min/max rule: length
|
|
328
|
+
PatternError, // regex mismatch rule: pattern
|
|
329
|
+
InvalidEmailError, // email format rule: email
|
|
330
|
+
InvalidUrlError, // URL format rule: url
|
|
455
331
|
} from '@nextrush/errors';
|
|
456
332
|
```
|
|
457
333
|
|
|
458
|
-
###
|
|
334
|
+
### Factory functions
|
|
459
335
|
|
|
460
|
-
```
|
|
461
|
-
import
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
ErrorMiddleware,
|
|
336
|
+
```ts
|
|
337
|
+
import {
|
|
338
|
+
badRequest, unauthorized, forbidden, notFound, methodNotAllowed,
|
|
339
|
+
conflict, unprocessableEntity, tooManyRequests,
|
|
340
|
+
internalError, serviceUnavailable, badGateway, gatewayTimeout,
|
|
341
|
+
createError,
|
|
467
342
|
} from '@nextrush/errors';
|
|
468
343
|
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
}
|
|
344
|
+
throw notFound('User not found');
|
|
345
|
+
throw tooManyRequests('Rate limit exceeded', { retryAfter: 60 });
|
|
483
346
|
```
|
|
484
347
|
|
|
485
|
-
|
|
348
|
+
## Options
|
|
486
349
|
|
|
487
|
-
|
|
350
|
+
`errorHandler(options?)` is the only configurable surface. Error classes take a plain `HttpErrorOptions` object (`code`, `expose`, `details`, `cause`, `requestId`, `traceId`, `timestamp`).
|
|
488
351
|
|
|
489
|
-
|
|
352
|
+
| Option | Type | Required | Default | Security-sensitive | Description |
|
|
353
|
+
| ------ | ---- | -------- | ------- | ------------------ | ----------- |
|
|
354
|
+
| `includeStack` | `boolean` | No | `false` | ⚠️ | Append the stack trace to the JSON body. Keep `false` in production — a stack trace leaks internal paths. |
|
|
355
|
+
| `logger` | `(error, ctx) => void` | No | logs 5xx as `error`, 4xx as `warn` | — | Custom sink for caught errors (route to your structured logger). |
|
|
356
|
+
| `transform` | `(error, ctx) => Record<string, unknown>` | No | the error's own `toJSON()` | — | Replace the serialized response body shape. |
|
|
357
|
+
| `handlers` | `Map<ErrorClass, (error, ctx) => void>` | No | `undefined` | — | Per-error-type handlers; the first `instanceof` match runs and short-circuits serialization. |
|
|
490
358
|
|
|
491
|
-
|
|
492
|
-
// ❌ Don't do this
|
|
493
|
-
throw new Error('User not found');
|
|
494
|
-
// → Returns 500 Internal Server Error, no error code
|
|
359
|
+
## Compatibility
|
|
495
360
|
|
|
496
|
-
|
|
497
|
-
throw new NotFoundError('User not found');
|
|
498
|
-
// → Returns 404 Not Found with NOT_FOUND code
|
|
499
|
-
````
|
|
361
|
+
**Requirements**
|
|
500
362
|
|
|
501
|
-
|
|
363
|
+
| Requirement | Version |
|
|
364
|
+
| ----------- | ------- |
|
|
365
|
+
| NextRush | `3.x` |
|
|
366
|
+
| Node.js | `>=22` |
|
|
367
|
+
| TypeScript | `>=5.x` |
|
|
502
368
|
|
|
503
|
-
|
|
369
|
+
**Runtimes**
|
|
504
370
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
});
|
|
371
|
+
| Runtime | Supported | Notes |
|
|
372
|
+
| ------- | --------- | ----- |
|
|
373
|
+
| Node.js `>=22` | ✅ | ESM-only |
|
|
374
|
+
| Bun / Deno / Edge | ✅ / ✅ / ✅ | Uses only the native `Error` and Web-standard JavaScript; `Error.captureStackTrace` is feature-detected and skipped where absent |
|
|
510
375
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
```
|
|
376
|
+
**Integration**
|
|
377
|
+
- **Peer dependencies:** none — depends only on `@nextrush/types` (types, erased at build).
|
|
378
|
+
- **Works with:** `@nextrush/core` (the middleware chain that runs `errorHandler()`), any `@nextrush/*` middleware that throws (`body-parser` → `413`, `rate-limit` → `429`).
|
|
379
|
+
- **Incompatible with:** none.
|
|
516
380
|
|
|
517
|
-
|
|
381
|
+
> [!IMPORTANT]
|
|
382
|
+
> NextRush is **ESM-only, permanently** — no CommonJS build. On Node `>=22`, CommonJS consumers
|
|
383
|
+
> can `require()` this ESM package natively. See the
|
|
384
|
+
> [Module Format Policy](https://github.com/0xTanzim/nextRush#module-format-policy).
|
|
518
385
|
|
|
519
|
-
|
|
386
|
+
---
|
|
520
387
|
|
|
521
|
-
|
|
522
|
-
// ❌ Errors won't be formatted
|
|
523
|
-
const app = createApp();
|
|
388
|
+
## Troubleshooting
|
|
524
389
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
});
|
|
390
|
+
<details>
|
|
391
|
+
<summary><strong>A thrown error returns <code>500</code> instead of my status code</strong></summary>
|
|
528
392
|
|
|
529
|
-
|
|
530
|
-
const app = createApp();
|
|
531
|
-
app.use(errorHandler()); // This catches and formats errors
|
|
393
|
+
**Cause:** you threw a generic `Error` (or `errorHandler()` isn't registered), so there's no `status` to read — the handler falls back to `500`. **Fix:** throw a typed error and register `errorHandler()` before your routes.
|
|
532
394
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
395
|
+
```ts
|
|
396
|
+
app.use(errorHandler()); // ← register first
|
|
397
|
+
throw new NotFoundError('User not found'); // ← typed, not `new Error(...)`
|
|
536
398
|
```
|
|
537
399
|
|
|
538
|
-
|
|
400
|
+
</details>
|
|
539
401
|
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
}
|
|
402
|
+
<details>
|
|
403
|
+
<summary><strong>My 5xx response shows "Internal Server Error", not my message</strong></summary>
|
|
547
404
|
|
|
548
|
-
|
|
549
|
-
async function getUser(id: string): Promise<User | null> {
|
|
550
|
-
return await db.findUser(id);
|
|
551
|
-
}
|
|
405
|
+
**Cause:** this is intended — 5xx errors default to `expose: false`, so `toJSON()` replaces the message to avoid leaking internals. The full error is still passed to the logger. **Fix:** only override `expose` for a message you've confirmed is client-safe.
|
|
552
406
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const user = await getUser(ctx.params.id);
|
|
556
|
-
if (!user) throw new NotFoundError('User not found');
|
|
557
|
-
ctx.json(user);
|
|
558
|
-
});
|
|
407
|
+
```ts
|
|
408
|
+
throw new ServiceUnavailableError('Down for maintenance until 14:00 UTC', { expose: true });
|
|
559
409
|
```
|
|
560
410
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
### Mistake 5: Missing Error Codes for API Clients
|
|
411
|
+
</details>
|
|
564
412
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
throw new BadRequestError('Invalid email format');
|
|
568
|
-
// Response: {"message": "Invalid email format"} // No code!
|
|
413
|
+
<details>
|
|
414
|
+
<summary><strong><code>isHttpError(validationError)</code> returns <code>false</code></strong></summary>
|
|
569
415
|
|
|
570
|
-
|
|
571
|
-
throw new BadRequestError('Invalid email format', {
|
|
572
|
-
code: 'INVALID_EMAIL',
|
|
573
|
-
});
|
|
574
|
-
// Response: {"message": "Invalid email format", "code": "INVALID_EMAIL"}
|
|
416
|
+
**Cause:** `ValidationError` extends `NextRushError` directly, not `HttpError`, so the `HttpError` type guard rejects it. **Fix:** use `getErrorStatus()` / `getSafeErrorMessage()` (which handle any `NextRushError`), or check `instanceof ValidationError`. `errorHandler()` already serializes it correctly.
|
|
575
417
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
418
|
+
```ts
|
|
419
|
+
import { getErrorStatus, ValidationError } from '@nextrush/errors';
|
|
420
|
+
if (err instanceof ValidationError) { /* read err.issues */ }
|
|
421
|
+
const status = getErrorStatus(err); // 400 for a ValidationError
|
|
580
422
|
```
|
|
581
423
|
|
|
582
|
-
|
|
424
|
+
</details>
|
|
583
425
|
|
|
584
|
-
|
|
426
|
+
<details>
|
|
427
|
+
<summary><strong><code>details</code> or <code>cause</code> is missing from the JSON response</strong></summary>
|
|
585
428
|
|
|
586
|
-
|
|
429
|
+
**Cause:** both are serialized only when the error is exposed (`expose === true`) — a non-exposed 5xx hides them to prevent leaking internal detail. **Fix:** read `error.cause` server-side (never gated), or set a client-safe error with `expose: true` if the detail is meant for the client.
|
|
587
430
|
|
|
588
|
-
|
|
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
|
-
}
|
|
431
|
+
</details>
|
|
594
432
|
|
|
595
|
-
|
|
596
|
-
export function validateEmail(email: string): { valid: boolean; error?: string } {
|
|
597
|
-
return isValid(email) ? { valid: true } : { valid: false, error: 'Invalid email format' };
|
|
598
|
-
}
|
|
433
|
+
## FAQ
|
|
599
434
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const result = validateEmail(ctx.body.email);
|
|
603
|
-
if (!result.valid) {
|
|
604
|
-
throw new BadRequestError(result.error!, { code: 'INVALID_EMAIL' });
|
|
605
|
-
}
|
|
606
|
-
});
|
|
607
|
-
```
|
|
435
|
+
**Can I use `@nextrush/errors` without the rest of NextRush?**
|
|
436
|
+
Yes. The error classes depend on nothing but the native `Error`. `errorHandler()` needs the `Context` / `Middleware` type contracts from `@nextrush/types`, which are erased at build — there's no runtime dependency to install.
|
|
608
437
|
|
|
609
|
-
**Why
|
|
438
|
+
**Why ESM-only?**
|
|
439
|
+
See the [Module Format Policy](https://github.com/0xTanzim/nextRush#module-format-policy).
|
|
610
440
|
|
|
611
|
-
|
|
441
|
+
**Does it work on Bun, Deno, and Edge?**
|
|
442
|
+
Yes. The classes use only the native `Error` and standard JavaScript; the V8-specific `Error.captureStackTrace` is feature-detected and skipped where it isn't available, so behavior is identical across runtimes.
|
|
612
443
|
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
}
|
|
444
|
+
**Why doesn't `ValidationError` extend `HttpError`?**
|
|
445
|
+
By design — a validation failure is a `NextRushError` that carries structured `issues`, not a status alone. It still resolves to `400`, is serialized by `errorHandler()`, and is recognized by `getErrorStatus()`; only the `isHttpError()` guard (which is `HttpError`-specific) returns `false` for it.
|
|
620
446
|
|
|
621
|
-
|
|
622
|
-
async function searchUsers(query: string): Promise<User[]> {
|
|
623
|
-
return await db.search(query); // Empty array is valid
|
|
624
|
-
}
|
|
447
|
+
---
|
|
625
448
|
|
|
626
|
-
|
|
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
|
-
```
|
|
449
|
+
## Package relationships
|
|
633
450
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
}
|
|
451
|
+
```text
|
|
452
|
+
depends on @nextrush/types (Context / Middleware / Next contracts, types only)
|
|
453
|
+
@nextrush/errors ─────────────▶
|
|
454
|
+
often used with @nextrush/core (runs errorHandler() in the middleware chain)
|
|
455
|
+
usually used next @nextrush/validation · @nextrush/body-parser (throw these errors)
|
|
650
456
|
```
|
|
651
457
|
|
|
652
|
-
|
|
458
|
+
- **Depends on:** [`@nextrush/types`](../types) — the shared `Context` / `Middleware` / `Next` contracts, used only by the middleware (types, erased at build).
|
|
459
|
+
- **Often used with:** [`@nextrush/core`](../core) — the `Application` whose middleware chain runs `errorHandler()`; every handler throws these classes.
|
|
460
|
+
- **Usually used next:** [`@nextrush/validation`](../middleware/validation) (surfaces `ValidationError`-shaped failures) · [`@nextrush/body-parser`](../middleware/body-parser) (throws `413`) · [`@nextrush/rate-limit`](../middleware/rate-limit) (throws `429`).
|
|
461
|
+
- **Alternative:** none — the `HttpError` hierarchy is the framework's error contract.
|
|
653
462
|
|
|
654
|
-
|
|
463
|
+
## Architecture
|
|
655
464
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
| Cloudflare Workers | ✅ | Full support |
|
|
662
|
-
| Vercel Edge Runtime | ✅ | Full support |
|
|
465
|
+
Maintaining or contributing to this package? The internal design — the `NextRushError` → `HttpError` /
|
|
466
|
+
`ValidationError` hierarchy, the `expose` privacy boundary, `cause`-chain serialization, the
|
|
467
|
+
throw→response lifecycle, the architectural invariants, and the decisions and trade-offs behind them
|
|
468
|
+
(with diagrams) — is in **[`ARCHITECTURE.md`](./ARCHITECTURE.md)**. Design history:
|
|
469
|
+
[ADR-0005 (package tiers & sealed surface)](https://github.com/0xTanzim/nextRush/blob/main/docs/adr/ADR-0005-package-tiers-sealed-surface-deprecation.md).
|
|
663
470
|
|
|
664
|
-
|
|
471
|
+
## Resources
|
|
665
472
|
|
|
666
|
-
|
|
473
|
+
- 📖 **Learn** — [Documentation](https://0xtanzim.github.io/nextRush/docs) · [Error handling guide](https://0xtanzim.github.io/nextRush/docs/guides/error-handling) · [Architecture](./ARCHITECTURE.md) · [RFCs](https://github.com/0xTanzim/nextRush/tree/main/docs/RFC)
|
|
474
|
+
- 📝 **Changelog** — [CHANGELOG.md](./CHANGELOG.md)
|
|
475
|
+
- 🐛 **Report an issue** — [GitHub Issues](https://github.com/0xTanzim/nextRush/issues)
|
|
476
|
+
- 🤝 **Contribute** — [CONTRIBUTING.md](https://github.com/0xTanzim/nextRush/blob/main/CONTRIBUTING.md)
|
|
667
477
|
|
|
668
|
-
|
|
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
|
|
478
|
+
---
|
|
674
479
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
MIT
|
|
678
|
-
|
|
679
|
-
```
|
|
680
|
-
|
|
681
|
-
```
|
|
480
|
+
MIT © [Tanzim Hossain](https://github.com/0xTanzim)
|