exisjs 0.7.12 → 0.7.14
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 +195 -39
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +3 -1
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/config/types.d.ts +2 -2
- package/dist/config/types.d.ts.map +1 -1
- package/dist/decorators/params.d.ts +21 -0
- package/dist/decorators/params.d.ts.map +1 -1
- package/dist/decorators/params.js +21 -1
- package/dist/decorators/params.js.map +1 -1
- package/dist/di/container.d.ts +15 -6
- package/dist/di/container.d.ts.map +1 -1
- package/dist/di/container.js +26 -3
- package/dist/di/container.js.map +1 -1
- package/dist/di/index.d.ts +1 -1
- package/dist/di/index.d.ts.map +1 -1
- package/dist/di/index.js.map +1 -1
- package/dist/di/inject.d.ts +2 -1
- package/dist/di/inject.d.ts.map +1 -1
- package/dist/di/inject.js +2 -1
- package/dist/di/inject.js.map +1 -1
- package/dist/docs/ARCHITECTURE.md +130 -78
- package/dist/docs/BENCHMARKS.md +4 -4
- package/dist/docs/content/README.md +43 -44
- package/dist/docs/content/boundaries.mdx +138 -61
- package/dist/docs/content/cli/commands.mdx +151 -128
- package/dist/docs/content/cli/overview.mdx +61 -47
- package/dist/docs/content/cli/usage.mdx +107 -30
- package/dist/docs/content/config.mdx +145 -158
- package/dist/docs/content/controllers.mdx +193 -464
- package/dist/docs/content/cron.mdx +183 -69
- package/dist/docs/content/decorators.mdx +449 -154
- package/dist/docs/content/errors.mdx +202 -305
- package/dist/docs/content/exisjs-mcp.mdx +59 -49
- package/dist/docs/content/installation.mdx +243 -157
- package/dist/docs/content/introduction.mdx +220 -48
- package/dist/docs/content/middleware.mdx +171 -497
- package/dist/docs/content/modules.mdx +141 -83
- package/dist/docs/content/providers.mdx +216 -180
- package/dist/docs/content/requests.mdx +168 -488
- package/dist/docs/content/responses.mdx +200 -533
- package/dist/docs/content/routing.mdx +235 -598
- package/dist/docs/content/security.mdx +233 -57
- package/dist/docs/content/structure.mdx +151 -240
- package/dist/docs/content/swagger.mdx +192 -91
- package/dist/docs/content/testing.mdx +205 -74
- package/dist/docs/content/validation.mdx +479 -341
- package/dist/docs/reference/README.md +34 -2
- package/dist/docs/reference/config.mdx +277 -75
- package/dist/docs/reference/context.mdx +197 -106
- package/dist/docs/reference/cron.mdx +266 -152
- package/dist/docs/reference/database.mdx +135 -53
- package/dist/docs/reference/decorators.mdx +234 -116
- package/dist/docs/reference/di.mdx +146 -112
- package/dist/docs/reference/error.mdx +155 -178
- package/dist/docs/reference/exis.mdx +141 -70
- package/dist/docs/reference/logger.mdx +110 -126
- package/dist/docs/reference/middleware.mdx +235 -158
- package/dist/docs/reference/plugin.mdx +136 -64
- package/dist/docs/reference/router.mdx +275 -293
- package/dist/docs/reference/sanitize.mdx +91 -87
- package/dist/docs/reference/swagger.mdx +280 -100
- package/dist/docs/reference/testing.mdx +223 -89
- package/dist/docs/reference/validator.mdx +194 -206
- package/dist/lib/start-server.js +22 -8
- package/dist/lib/start-server.js.map +1 -1
- package/dist/module/module.d.ts +6 -3
- package/dist/module/module.d.ts.map +1 -1
- package/dist/module/module.js +17 -3
- package/dist/module/module.js.map +1 -1
- package/dist/router/controller-registrar.d.ts.map +1 -1
- package/dist/router/controller-registrar.js +26 -4
- package/dist/router/controller-registrar.js.map +1 -1
- package/dist/router/scanner/fs-scanner.d.ts.map +1 -1
- package/dist/router/scanner/fs-scanner.js +22 -0
- package/dist/router/scanner/fs-scanner.js.map +1 -1
- package/dist/router/types.d.ts +6 -4
- package/dist/router/types.d.ts.map +1 -1
- package/dist/server/app.d.ts +3 -1
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +24 -8
- package/dist/server/app.js.map +1 -1
- package/dist/server/bootstrapper.d.ts.map +1 -1
- package/dist/server/bootstrapper.js +19 -0
- package/dist/server/bootstrapper.js.map +1 -1
- package/dist/server/request.d.ts +2 -0
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +11 -0
- package/dist/server/request.js.map +1 -1
- package/dist/server/response.d.ts +10 -5
- package/dist/server/response.d.ts.map +1 -1
- package/dist/server/response.js +65 -18
- package/dist/server/response.js.map +1 -1
- package/dist/swagger/generator.d.ts.map +1 -1
- package/dist/swagger/generator.js +130 -10
- package/dist/swagger/generator.js.map +1 -1
- package/dist/telemetry/index.d.ts +12 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +121 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/testing/client.d.ts +1 -0
- package/dist/testing/client.d.ts.map +1 -1
- package/dist/testing/client.js +4 -1
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +11 -4
- package/dist/testing/index.js.map +1 -1
- package/dist/validator/builder.d.ts +26 -1
- package/dist/validator/builder.d.ts.map +1 -1
- package/dist/validator/builder.js +31 -0
- package/dist/validator/builder.js.map +1 -1
- package/dist/validator/engine.d.ts +23 -0
- package/dist/validator/engine.d.ts.map +1 -1
- package/dist/validator/engine.js +228 -31
- package/dist/validator/engine.js.map +1 -1
- package/dist/validator/index.d.ts +8 -4
- package/dist/validator/index.d.ts.map +1 -1
- package/dist/validator/index.js +4 -1
- package/dist/validator/index.js.map +1 -1
- package/dist/validator/tex-types.d.ts +19 -4
- package/dist/validator/tex-types.d.ts.map +1 -1
- package/dist/validator/tex-types.js +102 -0
- package/dist/validator/tex-types.js.map +1 -1
- package/dist/validator/tex.d.ts +1 -1
- package/dist/validator/tex.d.ts.map +1 -1
- package/dist/validator/tex.js +3 -1
- package/dist/validator/tex.js.map +1 -1
- package/dist/validator/types.d.ts +1 -0
- package/dist/validator/types.d.ts.map +1 -1
- package/package.json +46 -14
- package/dist/docs/content/dataloader.mdx +0 -170
- package/dist/docs/reference/dataloader.mdx +0 -91
|
@@ -1,35 +1,88 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
title: "Error Handling and Exceptions"
|
|
3
|
+
description: "Standardize HTTP errors, exception filters, validation diagnostics, and uncaught exception handling in ExisJS."
|
|
4
|
+
nextUrl: "/docs/cron"
|
|
5
|
+
nextTitle: "Cron & Background Tasks"
|
|
4
6
|
---
|
|
5
7
|
|
|
6
8
|
# Error Handling
|
|
7
9
|
|
|
8
|
-
ExisJS
|
|
10
|
+
ExisJS provides an integrated exception architecture designed to standardize HTTP error responses, handle validation failures, and safely catch uncaught runtime exceptions across both functional and class-based applications.
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## The `HttpError` Hierarchy
|
|
15
|
+
|
|
16
|
+
All HTTP-related exceptions in ExisJS inherit from the base `HttpError` class. When an `HttpError` is thrown within a route handler or middleware, the router intercepts it and formats a consistent JSON error response with appropriate HTTP status codes and headers.
|
|
17
|
+
|
|
18
|
+
### Base `HttpError` Class
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
export class HttpError extends Error {
|
|
22
|
+
public readonly statusCode: number
|
|
23
|
+
public readonly code: string
|
|
24
|
+
public readonly isOperational: boolean
|
|
25
|
+
public readonly details?: unknown
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
message: string,
|
|
29
|
+
statusCode = 500,
|
|
30
|
+
code = 'INTERNAL_ERROR',
|
|
31
|
+
details?: unknown
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- **`statusCode`**: The HTTP status code returned to the client (e.g., `404`, `401`, `500`).
|
|
37
|
+
- **`code`**: A machine-readable uppercase string identifying the error category (e.g., `'NOT_FOUND'`, `'BAD_REQUEST'`).
|
|
38
|
+
- **`isOperational`**: A boolean flag (`true` for `HttpError`) indicating expected operational errors versus programmer bugs or unhandled runtime crashes.
|
|
39
|
+
- **`details`**: Optional structured context or validation metadata attached to the error.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Standard Error Classes & Exception Aliases
|
|
44
|
+
|
|
45
|
+
ExisJS exports dedicated error classes alongside standard `*Exception` aliases for compatibility with both idiomatic TypeScript and NestJS-style OOP architectures.
|
|
46
|
+
|
|
47
|
+
| HTTP Status | Error Class | Exception Alias | Default Error Code | Static Factory Method |
|
|
48
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
49
|
+
| **`400`** | `BadRequestError` | `BadRequestException` | `BAD_REQUEST` | `HttpError.badRequest(msg, details?)` |
|
|
50
|
+
| **`401`** | `UnauthorizedError` | `UnauthorizedException` | `UNAUTHORIZED` | `HttpError.unauthorized(msg?)` |
|
|
51
|
+
| **`403`** | `ForbiddenError` | `ForbiddenException` | `FORBIDDEN` | `HttpError.forbidden(msg?)` |
|
|
52
|
+
| **`404`** | `NotFoundError` | `NotFoundException` | `NOT_FOUND` | `HttpError.notFound(resource?)` |
|
|
53
|
+
| **`409`** | `ConflictError` | `ConflictException` | `CONFLICT` | `HttpError.conflict(msg)` |
|
|
54
|
+
| **`413`** | `PayloadTooLargeError` | `PayloadTooLargeException` | `PAYLOAD_TOO_LARGE` | `HttpError.payloadTooLarge(msg?, details?)` |
|
|
55
|
+
| **`422`** | `UnprocessableError` | `UnprocessableException` | `UNPROCESSABLE_ENTITY` | `HttpError.unprocessable(msg, details?)` |
|
|
56
|
+
| **`429`** | `RateLimitError` | `RateLimitException` | `RATE_LIMITED` | `HttpError.tooManyRequests(msg?)` |
|
|
57
|
+
| **`500`** | `InternalError` | `InternalException` | `INTERNAL_ERROR` | `HttpError.internal(msg?)` |
|
|
58
|
+
| **`503`** | `ServiceUnavailableError` | `ServiceUnavailableException` | `SERVICE_UNAVAILABLE` | `HttpError.serviceUnavailable(msg?)` |
|
|
59
|
+
| **`504`** | `GatewayTimeoutError` | `GatewayTimeoutException` | `GATEWAY_TIMEOUT` | `HttpError.gatewayTimeout(msg?)` |
|
|
60
|
+
|
|
61
|
+
---
|
|
11
62
|
|
|
12
|
-
|
|
63
|
+
## Throwing Errors in Handlers
|
|
64
|
+
|
|
65
|
+
You can throw errors directly in controllers, route definitions, or services using either static factory methods, direct instantiation of error classes, or exception aliases.
|
|
13
66
|
|
|
14
67
|
### Functional Paradigm
|
|
15
68
|
|
|
16
69
|
```typescript filename="src/http/users/route.ts"
|
|
17
70
|
import { controller, route } from 'exisjs/router'
|
|
18
|
-
import { HttpError } from 'exisjs/error'
|
|
71
|
+
import { HttpError, NotFoundError, ForbiddenException } from 'exisjs/error'
|
|
19
72
|
|
|
20
73
|
export default controller({
|
|
21
74
|
getUser: route.get('/:id', {
|
|
22
75
|
async handle({ params }) {
|
|
23
|
-
const user = db.
|
|
76
|
+
const user = await db.users.findById(params.id)
|
|
24
77
|
|
|
25
78
|
if (!user) {
|
|
26
|
-
//
|
|
27
|
-
throw HttpError.notFound('User')
|
|
79
|
+
// Option 1: Static factory helper
|
|
80
|
+
throw HttpError.notFound('User')
|
|
28
81
|
}
|
|
29
82
|
|
|
30
|
-
if (user.
|
|
31
|
-
//
|
|
32
|
-
throw
|
|
83
|
+
if (user.isSuspended) {
|
|
84
|
+
// Option 2: Direct exception alias
|
|
85
|
+
throw new ForbiddenException('User account has been suspended')
|
|
33
86
|
}
|
|
34
87
|
|
|
35
88
|
return { user }
|
|
@@ -42,22 +95,20 @@ export default controller({
|
|
|
42
95
|
|
|
43
96
|
```typescript filename="src/http/users/route.ts"
|
|
44
97
|
import { Controller, Get, Param } from 'exisjs/decorators'
|
|
45
|
-
import {
|
|
98
|
+
import { NotFoundException, ForbiddenException } from 'exisjs/error'
|
|
46
99
|
|
|
47
|
-
@Controller()
|
|
100
|
+
@Controller('/users')
|
|
48
101
|
export default class UsersController {
|
|
49
102
|
@Get('/:id')
|
|
50
103
|
async getUser(@Param('id') id: string) {
|
|
51
|
-
const user = db.
|
|
104
|
+
const user = await db.users.findById(id)
|
|
52
105
|
|
|
53
106
|
if (!user) {
|
|
54
|
-
|
|
55
|
-
throw HttpError.notFound('User')
|
|
107
|
+
throw new NotFoundException('User')
|
|
56
108
|
}
|
|
57
109
|
|
|
58
|
-
if (user.
|
|
59
|
-
|
|
60
|
-
throw HttpError.forbidden('User is banned from the platform')
|
|
110
|
+
if (user.isSuspended) {
|
|
111
|
+
throw new ForbiddenException('User account has been suspended')
|
|
61
112
|
}
|
|
62
113
|
|
|
63
114
|
return { user }
|
|
@@ -65,141 +116,109 @@ export default class UsersController {
|
|
|
65
116
|
}
|
|
66
117
|
```
|
|
67
118
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
The `HttpError` class provides static factory methods for standard HTTP status codes:
|
|
71
|
-
|
|
72
|
-
- `HttpError.badRequest('Message')` - 400
|
|
73
|
-
- `HttpError.unauthorized('Message')` - 401
|
|
74
|
-
- `HttpError.forbidden('Message')` - 403
|
|
75
|
-
- `HttpError.notFound('Resource')` - 404
|
|
76
|
-
- `HttpError.conflict('Message')` - 409
|
|
77
|
-
- `HttpError.payloadTooLarge('Message')` - 413
|
|
78
|
-
- `HttpError.unprocessable('Message', details)` - 422
|
|
79
|
-
- `HttpError.rateLimit('Message')` - 429
|
|
80
|
-
- `HttpError.internal('Message')` - 500
|
|
81
|
-
|
|
82
|
-
### Direct Error Classes & Exceptions
|
|
83
|
-
|
|
84
|
-
If you prefer a class-based approach, ExisJS exports every standard error as its own class. We also provide `Exception` aliases for every error class. You can import and throw them directly:
|
|
85
|
-
|
|
86
|
-
```typescript filename="example.ts"
|
|
87
|
-
import {
|
|
88
|
-
NotFoundException,
|
|
89
|
-
UnauthorizedException,
|
|
90
|
-
BadRequestError,
|
|
91
|
-
PayloadTooLargeError
|
|
92
|
-
} from 'exisjs/error'
|
|
93
|
-
|
|
94
|
-
export const uploadMedia = (req, res) => {
|
|
95
|
-
if (req.headers['content-length'] > 10 * 1024 * 1024) {
|
|
96
|
-
throw new PayloadTooLargeError('File exceeds 10MB limit')
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
```
|
|
119
|
+
---
|
|
100
120
|
|
|
101
|
-
|
|
121
|
+
## Standard Error Response Format
|
|
102
122
|
|
|
103
|
-
When an `HttpError` is thrown
|
|
123
|
+
When an `HttpError` is thrown, ExisJS returns a standardized JSON payload:
|
|
104
124
|
|
|
105
|
-
```json filename="HTTP
|
|
125
|
+
```json filename="HTTP 404 Response"
|
|
106
126
|
{
|
|
107
127
|
"success": false,
|
|
108
128
|
"statusCode": 404,
|
|
109
129
|
"error": "Not Found",
|
|
110
130
|
"message": "User not found",
|
|
111
131
|
"validationErrors": null,
|
|
112
|
-
"timestamp": "2026-
|
|
132
|
+
"timestamp": "2026-10-02T12:45:00.000Z"
|
|
113
133
|
}
|
|
114
134
|
```
|
|
115
135
|
|
|
116
|
-
### Validation Error Responses
|
|
136
|
+
### Validation Error Responses
|
|
117
137
|
|
|
118
|
-
When schema validation fails (via `tex`, Zod, or Yup), ExisJS
|
|
138
|
+
When schema validation fails (via `tex`, Zod, or Yup), ExisJS intercepts the validation failure and generates a structured `400 Bad Request` payload containing field-level error mappings:
|
|
119
139
|
|
|
120
|
-
```json filename="HTTP 400
|
|
140
|
+
```json filename="HTTP 400 Validation Error Response"
|
|
121
141
|
{
|
|
122
142
|
"success": false,
|
|
123
143
|
"statusCode": 400,
|
|
124
144
|
"error": "Bad Request",
|
|
125
|
-
"message": "Validation Error: email: Invalid email format, age:
|
|
145
|
+
"message": "Validation Error: email: Invalid email format, age: Minimum age is 18",
|
|
126
146
|
"validationErrors": {
|
|
127
147
|
"email": "Invalid email format",
|
|
128
|
-
"age": "
|
|
148
|
+
"age": "Minimum age is 18"
|
|
129
149
|
},
|
|
130
150
|
"errors": {
|
|
131
151
|
"email": "Invalid email format",
|
|
132
|
-
"age": "
|
|
152
|
+
"age": "Minimum age is 18"
|
|
133
153
|
},
|
|
134
|
-
"
|
|
154
|
+
"details": [
|
|
155
|
+
{
|
|
156
|
+
"field": "email",
|
|
157
|
+
"message": "Invalid email format",
|
|
158
|
+
"expected": "Valid email address",
|
|
159
|
+
"received": "invalid-email",
|
|
160
|
+
"code": "INVALID_EMAIL",
|
|
161
|
+
"httpPart": "body"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"field": "age",
|
|
165
|
+
"message": "Minimum age is 18",
|
|
166
|
+
"expected": ">= 18",
|
|
167
|
+
"received": "16",
|
|
168
|
+
"code": "MIN_VALUE",
|
|
169
|
+
"httpPart": "body"
|
|
170
|
+
}
|
|
171
|
+
],
|
|
172
|
+
"timestamp": "2026-10-02T12:45:00.000Z"
|
|
135
173
|
}
|
|
136
174
|
```
|
|
137
175
|
|
|
138
|
-
|
|
176
|
+
### Development Terminal Diagnostics
|
|
139
177
|
|
|
140
|
-
In development mode,
|
|
178
|
+
In development mode (`NODE_ENV !== 'production'`), validation failures output a formatted diagnostic table to the console identifying the failed HTTP part (`body`, `query`, `params`, `headers`), field names, received values, and expected criteria:
|
|
141
179
|
|
|
142
180
|
```text
|
|
143
|
-
[
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
│
|
|
148
|
-
|
|
149
|
-
|
|
181
|
+
[Validation Failed] POST /auth/register (part: body)
|
|
182
|
+
✖ email: Invalid email format
|
|
183
|
+
✖ age: Minimum age is 18
|
|
184
|
+
┌─────────────┬───────────────────────────┬──────────────────────────┐
|
|
185
|
+
│ Field │ Received │ Expected │
|
|
186
|
+
├─────────────┼───────────────────────────┼──────────────────────────┤
|
|
187
|
+
│ email │ "invalid-email" │ Valid email address │
|
|
188
|
+
│ age │ "16" │ >= 18 │
|
|
189
|
+
└─────────────┴───────────────────────────┴──────────────────────────┘
|
|
150
190
|
```
|
|
151
191
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
## Error Handling Decision Tree
|
|
155
|
-
|
|
156
|
-
To keep your codebase clean and consistent, use this decision tree when deciding how to handle errors:
|
|
157
|
-
|
|
158
|
-
1. **Is it a known client error (e.g., missing parameter, forbidden)?**
|
|
159
|
-
👉 **Throw an `HttpError` directly.** Let the framework format the 4xx response automatically.
|
|
160
|
-
2. **Do you need to intercept an unexpected failure (e.g., Database Connection failure) globally?**
|
|
161
|
-
👉 **Use an Exception Filter (`catchError`) in `server.ts`.** Map the specific exception class to a 5xx response.
|
|
162
|
-
3. **Do you need to intercept errors only for a specific route group (e.g., Admin Panel)?**
|
|
163
|
-
👉 **Use Local Error Hooks (`onError`) in the gateway/controller.** Keep the logic isolated to that specific module.
|
|
164
|
-
4. **Is it standard 4-arg middleware from the Express ecosystem?**
|
|
165
|
-
👉 **Use `app.use(errorHandler)`** (where `errorHandler = (err, req, res, next) => {}`). ExisJS supports standard Connect/Express error middleware natively.
|
|
166
|
-
|
|
167
|
-
## Extended Error Handling
|
|
168
|
-
|
|
169
|
-
ExisJS provides mechanisms for granular and centralized error handling.
|
|
170
|
-
|
|
171
|
-
### 1. Framework-Level Exception Handler (`src/http/error.ts`)
|
|
192
|
+
---
|
|
172
193
|
|
|
173
|
-
|
|
194
|
+
## Centralized Exception Handling (`src/http/error.ts`)
|
|
174
195
|
|
|
175
|
-
|
|
196
|
+
ExisJS supports an application-wide error handler convention located at `src/http/error.ts`. When this file is present, the framework routes all unhandled exceptions, validation errors, and uncaught rejections through this handler before sending the final HTTP response.
|
|
176
197
|
|
|
177
|
-
|
|
198
|
+
### Functional Paradigm
|
|
178
199
|
|
|
179
200
|
```typescript filename="src/http/error.ts"
|
|
180
|
-
import type {
|
|
181
|
-
import { isDev } from 'exisjs/config'
|
|
201
|
+
import type { ExisRequest, ExisResponse } from 'exisjs'
|
|
182
202
|
|
|
183
203
|
/**
|
|
184
|
-
* Global
|
|
185
|
-
*
|
|
204
|
+
* Global application error handler.
|
|
205
|
+
* Automatically mounted during server bootstrap.
|
|
186
206
|
*/
|
|
187
|
-
export function onError(err: any, req:
|
|
207
|
+
export function onError(err: any, req: ExisRequest, res: ExisResponse) {
|
|
188
208
|
const status = err.statusCode || err.status || 500
|
|
189
|
-
const isOperational = Boolean(err.isOperational ||
|
|
209
|
+
const isOperational = Boolean(err.isOperational || status < 500)
|
|
190
210
|
|
|
191
|
-
//
|
|
192
|
-
if (!isOperational &&
|
|
193
|
-
|
|
194
|
-
req.log.error({ err }, '[Fatal] Unhandled Server Exception')
|
|
211
|
+
// Log non-operational server crashes to telemetry / APM
|
|
212
|
+
if (!isOperational && process.env.NODE_ENV === 'production') {
|
|
213
|
+
req.log.error({ err, path: req.path, method: req.method }, 'Unhandled server crash')
|
|
195
214
|
}
|
|
196
215
|
|
|
197
|
-
// 2. Format a unified, standard error envelope for client applications
|
|
198
216
|
res.status(status).json({
|
|
199
217
|
success: false,
|
|
218
|
+
statusCode: status,
|
|
200
219
|
error: {
|
|
201
220
|
code: err.code || (status === 500 ? 'INTERNAL_SERVER_ERROR' : 'ERROR'),
|
|
202
|
-
message: err.message || '
|
|
221
|
+
message: err.message || 'An unexpected error occurred',
|
|
203
222
|
...(err.details ? { details: err.details } : {}),
|
|
204
223
|
...(err.errors ? { errors: err.errors } : {}),
|
|
205
224
|
path: req.path,
|
|
@@ -209,26 +228,26 @@ export function onError(err: any, req: Request, res: Response) {
|
|
|
209
228
|
}
|
|
210
229
|
```
|
|
211
230
|
|
|
212
|
-
|
|
231
|
+
### Class-Based (OOP) Paradigm
|
|
213
232
|
|
|
214
233
|
```typescript filename="src/http/error.ts"
|
|
215
|
-
import type {
|
|
216
|
-
import { isDev } from 'exisjs/config'
|
|
234
|
+
import type { ExisRequest, ExisResponse } from 'exisjs'
|
|
217
235
|
|
|
218
236
|
export default class GlobalErrorHandler {
|
|
219
|
-
onError(err: any, req:
|
|
237
|
+
onError(err: any, req: ExisRequest, res: ExisResponse) {
|
|
220
238
|
const status = err.statusCode || err.status || 500
|
|
221
|
-
const isOperational = Boolean(err.isOperational ||
|
|
239
|
+
const isOperational = Boolean(err.isOperational || status < 500)
|
|
222
240
|
|
|
223
|
-
if (!isOperational &&
|
|
224
|
-
req.log.error({ err }, '
|
|
241
|
+
if (!isOperational && process.env.NODE_ENV === 'production') {
|
|
242
|
+
req.log.error({ err, path: req.path, method: req.method }, 'Unhandled server crash')
|
|
225
243
|
}
|
|
226
244
|
|
|
227
245
|
res.status(status).json({
|
|
228
246
|
success: false,
|
|
247
|
+
statusCode: status,
|
|
229
248
|
error: {
|
|
230
249
|
code: err.code || (status === 500 ? 'INTERNAL_SERVER_ERROR' : 'ERROR'),
|
|
231
|
-
message: err.message || '
|
|
250
|
+
message: err.message || 'An unexpected error occurred',
|
|
232
251
|
...(err.details ? { details: err.details } : {}),
|
|
233
252
|
path: req.path,
|
|
234
253
|
timestamp: new Date().toISOString(),
|
|
@@ -238,249 +257,127 @@ export default class GlobalErrorHandler {
|
|
|
238
257
|
}
|
|
239
258
|
```
|
|
240
259
|
|
|
241
|
-
|
|
242
|
-
> **Scaffold with CLI**: You can generate this file instantly using the ExisJS CLI:
|
|
243
|
-
> ```bash
|
|
244
|
-
> npx exis generate error
|
|
245
|
-
> # or shorthand:
|
|
246
|
-
> npx exis g error
|
|
247
|
-
> ```
|
|
248
|
-
|
|
249
|
-
#### What `src/http/error.ts` Intercepts:
|
|
250
|
-
- **`HttpError` instances**: (e.g. `throw HttpError.notFound()`, `throw HttpError.unauthorized()`).
|
|
251
|
-
- **Validation failures**: Zod, Tex, or Yup validation rejections automatically pass structured `err.errors` and `err.details`.
|
|
252
|
-
- **Database / Network exceptions**: Postgres connection failures, timeout rejections, and third-party API errors.
|
|
253
|
-
- **Syntax errors**: Malformed JSON bodies or bad request payloads.
|
|
254
|
-
|
|
260
|
+
---
|
|
255
261
|
|
|
256
|
-
|
|
262
|
+
## Exception Filters (`@Catch` & `catchError`)
|
|
257
263
|
|
|
258
|
-
|
|
264
|
+
Exception filters allow you to bind error-handling logic to specific exception types, preventing repetitive `try/catch` boilerplate across service layers.
|
|
259
265
|
|
|
260
|
-
|
|
266
|
+
### Middleware-Based Exception Filter (`catchError`)
|
|
261
267
|
|
|
262
|
-
|
|
268
|
+
Use the `catchError` middleware to intercept specific error classes at the server or boundary level:
|
|
263
269
|
|
|
264
270
|
```typescript filename="src/http/server.ts"
|
|
265
271
|
import { exis } from 'exisjs'
|
|
266
272
|
import { catchError } from 'exisjs/middleware'
|
|
267
273
|
|
|
268
|
-
export class
|
|
269
|
-
constructor(message
|
|
274
|
+
export class DatabaseTimeoutError extends Error {
|
|
275
|
+
constructor(message = 'Database query timed out') {
|
|
270
276
|
super(message)
|
|
271
|
-
this.name = '
|
|
277
|
+
this.name = 'DatabaseTimeoutError'
|
|
272
278
|
}
|
|
273
279
|
}
|
|
274
280
|
|
|
275
281
|
export default exis({
|
|
276
282
|
async onStart(app) {
|
|
277
|
-
//
|
|
278
|
-
app.use(catchError(
|
|
283
|
+
// Intercept DatabaseTimeoutError globally and map to a 503 response
|
|
284
|
+
app.use(catchError(DatabaseTimeoutError, (err, req, res) => {
|
|
279
285
|
res.status(503).json({
|
|
280
286
|
success: false,
|
|
281
|
-
|
|
287
|
+
statusCode: 503,
|
|
288
|
+
error: {
|
|
289
|
+
code: 'DATABASE_UNAVAILABLE',
|
|
290
|
+
message: err.message,
|
|
291
|
+
},
|
|
282
292
|
})
|
|
283
293
|
}))
|
|
284
294
|
}
|
|
285
295
|
})
|
|
286
296
|
```
|
|
287
297
|
|
|
288
|
-
###
|
|
298
|
+
### OOP Exception Filters (`@Catch` & `ExceptionFilter`)
|
|
289
299
|
|
|
290
|
-
|
|
291
|
-
import { Server } from 'exisjs/decorators'
|
|
292
|
-
import { catchError } from 'exisjs/middleware'
|
|
293
|
-
import type { App } from 'exisjs'
|
|
300
|
+
In class-based applications, implement the `ExceptionFilter<T>` interface and bind it using `@UseFilters()` or `@Use()`:
|
|
294
301
|
|
|
295
|
-
|
|
296
|
-
constructor(message: string) {
|
|
297
|
-
super(message)
|
|
298
|
-
this.name = 'DatabaseConnectionError'
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
@Server()
|
|
303
|
-
export default class RootServer {
|
|
304
|
-
async onStart(app: App) {
|
|
305
|
-
// Globally intercept DatabaseConnectionError
|
|
306
|
-
app.use(catchError(DatabaseConnectionError, (err, req, res) => {
|
|
307
|
-
res.status(503).json({
|
|
308
|
-
success: false,
|
|
309
|
-
error: { code: 'DB_OFFLINE', message: err.message }
|
|
310
|
-
})
|
|
311
|
-
}))
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
This prevents you from having to write `try/catch` blocks in every single route just to handle standard database failures.
|
|
317
|
-
|
|
318
|
-
### 2. Local Error Hooks (`onError`)
|
|
319
|
-
|
|
320
|
-
If you want to catch errors only within a specific module or route grouping, you can use the `onError` hook directly in your controller definition.
|
|
321
|
-
|
|
322
|
-
When a controller throws an error, the local `onError` hook will intercept it before it reaches the global error handler.
|
|
323
|
-
|
|
324
|
-
### Functional Paradigm
|
|
325
|
-
|
|
326
|
-
```typescript filename="src/http/users/route.ts"
|
|
327
|
-
import { controller, route } from 'exisjs/router'
|
|
328
|
-
|
|
329
|
-
export default controller({
|
|
330
|
-
// This local hook catches errors thrown by the routes in this controller
|
|
331
|
-
onError: (err, req, res) => {
|
|
332
|
-
console.log('Users Controller Error Handler Caught:', err.message)
|
|
333
|
-
res.status(400).json({ success: false, localError: err.message })
|
|
334
|
-
},
|
|
335
|
-
|
|
336
|
-
crash: route.get('/crash', async () => {
|
|
337
|
-
// This error will be caught by the local onError hook
|
|
338
|
-
throw new Error("Something went wrong in the users module")
|
|
339
|
-
})
|
|
340
|
-
})
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
### Class-Based (OOP) Paradigm
|
|
344
|
-
|
|
345
|
-
You can use function handlers or structured class-based `ExceptionFilter` implementations with `@Catch()`:
|
|
346
|
-
|
|
347
|
-
```typescript filename="src/http/filters/not-found.filter.ts"
|
|
302
|
+
```typescript filename="src/http/filters/database.filter.ts"
|
|
348
303
|
import { Catch, type ExceptionFilter, type ArgumentsHost } from 'exisjs/decorators'
|
|
349
|
-
import {
|
|
304
|
+
import { DatabaseTimeoutError } from '../errors/database-timeout.error'
|
|
350
305
|
|
|
351
|
-
@Catch(
|
|
352
|
-
export class
|
|
353
|
-
catch(exception:
|
|
306
|
+
@Catch(DatabaseTimeoutError)
|
|
307
|
+
export class DatabaseExceptionFilter implements ExceptionFilter<DatabaseTimeoutError> {
|
|
308
|
+
catch(exception: DatabaseTimeoutError, host: ArgumentsHost) {
|
|
354
309
|
const { res } = host
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
310
|
+
|
|
311
|
+
res.status(503).json({
|
|
312
|
+
success: false,
|
|
313
|
+
statusCode: 503,
|
|
314
|
+
error: {
|
|
315
|
+
code: 'DATABASE_TIMEOUT',
|
|
316
|
+
message: exception.message,
|
|
317
|
+
},
|
|
359
318
|
timestamp: new Date().toISOString(),
|
|
360
319
|
})
|
|
361
320
|
}
|
|
362
321
|
}
|
|
363
322
|
```
|
|
364
323
|
|
|
324
|
+
Attach the filter to a controller or specific route method:
|
|
325
|
+
|
|
365
326
|
```typescript filename="src/http/users/route.ts"
|
|
366
|
-
import { Controller, Get, UseFilters
|
|
367
|
-
import {
|
|
368
|
-
import { NotFoundExceptionFilter } from '../filters/not-found.filter'
|
|
327
|
+
import { Controller, Get, UseFilters } from 'exisjs/decorators'
|
|
328
|
+
import { DatabaseExceptionFilter } from '../filters/database.filter'
|
|
369
329
|
|
|
370
330
|
@Controller('/users')
|
|
371
|
-
@
|
|
331
|
+
@UseFilters(DatabaseExceptionFilter)
|
|
372
332
|
export default class UsersController {
|
|
373
|
-
@Get('
|
|
374
|
-
async
|
|
375
|
-
|
|
333
|
+
@Get('/metrics')
|
|
334
|
+
async getMetrics() {
|
|
335
|
+
// If this throws DatabaseTimeoutError, DatabaseExceptionFilter handles it
|
|
336
|
+
return await db.queryMetrics()
|
|
376
337
|
}
|
|
377
338
|
}
|
|
378
339
|
```
|
|
379
340
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
## Observability (BYOM/BYOT)
|
|
383
|
-
|
|
384
|
-
ExisJS ships with tracing and metric adapters. Instead of tying you to a specific telemetry ecosystem, ExisJS lets you **Bring Your Own Metrics** (BYOM) and **Bring Your Own Tracer** (BYOT).
|
|
341
|
+
---
|
|
385
342
|
|
|
386
|
-
|
|
343
|
+
## Local Controller Error Handlers (`onError`)
|
|
387
344
|
|
|
388
|
-
|
|
345
|
+
For localized error boundaries that apply only to routes within a single controller, define an `onError` hook directly on the controller.
|
|
389
346
|
|
|
390
347
|
### Functional Paradigm
|
|
391
348
|
|
|
392
|
-
```typescript filename="src/http/
|
|
393
|
-
import {
|
|
394
|
-
import { metrics } from 'exisjs/observability'
|
|
395
|
-
|
|
396
|
-
const myPrometheusAdapter = {
|
|
397
|
-
onRequestStart: ({ method, path }) => {
|
|
398
|
-
// A request just started. Start a timer.
|
|
399
|
-
return ({ statusCode, durationMs }) => {
|
|
400
|
-
// The request just finished. Push to Prometheus.
|
|
401
|
-
console.log(`[Metrics] ${method} ${path} took ${durationMs}ms with status ${statusCode}`)
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
export const config = defineBoundary({
|
|
407
|
-
middleware: [metrics(myPrometheusAdapter)]
|
|
408
|
-
})
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
### Class-Based (OOP) Paradigm
|
|
412
|
-
|
|
413
|
-
```typescript filename="src/http/boundary.ts"
|
|
414
|
-
import { Boundary } from 'exisjs/decorators'
|
|
415
|
-
import { metrics } from 'exisjs/observability'
|
|
416
|
-
|
|
417
|
-
const myPrometheusAdapter = {
|
|
418
|
-
onRequestStart: ({ method, path }) => {
|
|
419
|
-
return ({ statusCode, durationMs }) => {
|
|
420
|
-
console.log(`[Metrics] ${method} ${path} took ${durationMs}ms with status ${statusCode}`)
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
@Boundary({
|
|
426
|
-
middleware: [metrics(myPrometheusAdapter)]
|
|
427
|
-
})
|
|
428
|
-
export default class RootBoundary {}
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
*Note: ExisJS normalizes dynamic paths (e.g., turning `/users/123` into `/users/:id`) to prevent metrics cardinality issues.*
|
|
432
|
-
|
|
433
|
-
### OpenTelemetry Tracing
|
|
434
|
-
|
|
435
|
-
ExisJS seamlessly integrates with OpenTelemetry (OTel) via the `tracing` middleware.
|
|
436
|
-
|
|
437
|
-
### Functional Paradigm
|
|
349
|
+
```typescript filename="src/http/billing/route.ts"
|
|
350
|
+
import { controller, route } from 'exisjs/router'
|
|
438
351
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
setStatus: (status) => span.setStatus(status),
|
|
451
|
-
recordException: (e) => span.recordException(e),
|
|
452
|
-
end: () => span.end()
|
|
352
|
+
export default controller({
|
|
353
|
+
// Intercepts all errors thrown by routes in this controller
|
|
354
|
+
onError(err, req, res) {
|
|
355
|
+
req.log.warn({ err }, 'Billing operation failed')
|
|
356
|
+
res.status(402).json({
|
|
357
|
+
success: false,
|
|
358
|
+
statusCode: 402,
|
|
359
|
+
error: {
|
|
360
|
+
code: 'PAYMENT_REQUIRED',
|
|
361
|
+
message: err.message,
|
|
362
|
+
},
|
|
453
363
|
})
|
|
454
|
-
}
|
|
455
|
-
}
|
|
364
|
+
},
|
|
456
365
|
|
|
457
|
-
|
|
458
|
-
|
|
366
|
+
checkout: route.post('/checkout', {
|
|
367
|
+
async handle({ body }) {
|
|
368
|
+
throw new Error('Insufficient funds')
|
|
369
|
+
}
|
|
370
|
+
})
|
|
459
371
|
})
|
|
460
372
|
```
|
|
461
373
|
|
|
462
|
-
|
|
374
|
+
---
|
|
463
375
|
|
|
464
|
-
|
|
465
|
-
import { Boundary } from 'exisjs/decorators'
|
|
466
|
-
import { tracing } from 'exisjs/observability'
|
|
467
|
-
|
|
468
|
-
const myOtelAdapter = {
|
|
469
|
-
startActiveSpan: (name, metadata, callback) => {
|
|
470
|
-
const span = tracer.startSpan(name)
|
|
471
|
-
callback({
|
|
472
|
-
setAttribute: (k, v) => span.setAttribute(k, v),
|
|
473
|
-
setStatus: (status) => span.setStatus(status),
|
|
474
|
-
recordException: (e) => span.recordException(e),
|
|
475
|
-
end: () => span.end()
|
|
476
|
-
})
|
|
477
|
-
}
|
|
478
|
-
}
|
|
376
|
+
## Unhandled Errors in Production vs Development
|
|
479
377
|
|
|
480
|
-
|
|
481
|
-
middleware: [tracing(myOtelAdapter)]
|
|
482
|
-
})
|
|
483
|
-
export default class RootBoundary {}
|
|
484
|
-
```
|
|
378
|
+
ExisJS handles uncaught non-operational errors (such as unexpected `TypeError` or unhandled promise rejections) based on `NODE_ENV`:
|
|
485
379
|
|
|
486
|
-
|
|
380
|
+
- **Production (`NODE_ENV === 'production'`)**: Returns a generic `500 Internal Server Error` without leaking stack traces, internal variables, or system paths. The complete stack trace is written to the structured logger.
|
|
381
|
+
- **Development (`NODE_ENV !== 'production'`)**:
|
|
382
|
+
- For JSON clients (`Accept: application/json`): Returns the error message and complete stack trace in the JSON envelope.
|
|
383
|
+
- For browser clients (`Accept: text/html`): Renders an interactive diagnostic error overlay with visual code frames and file locations.
|