te.js 2.0.3 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +197 -187
  2. package/auto-docs/analysis/handler-analyzer.js +58 -58
  3. package/auto-docs/analysis/source-resolver.js +101 -101
  4. package/auto-docs/constants.js +37 -37
  5. package/auto-docs/docs-llm/index.js +7 -0
  6. package/auto-docs/{llm → docs-llm}/prompts.js +222 -222
  7. package/auto-docs/{llm → docs-llm}/provider.js +132 -187
  8. package/auto-docs/index.js +146 -146
  9. package/auto-docs/openapi/endpoint-processor.js +277 -277
  10. package/auto-docs/openapi/generator.js +107 -107
  11. package/auto-docs/openapi/level3.js +131 -131
  12. package/auto-docs/openapi/spec-builders.js +244 -244
  13. package/auto-docs/ui/docs-ui.js +186 -186
  14. package/auto-docs/utils/logger.js +17 -17
  15. package/auto-docs/utils/strip-usage.js +10 -10
  16. package/cli/docs-command.js +315 -315
  17. package/cli/fly-command.js +71 -71
  18. package/cli/index.js +56 -56
  19. package/database/index.js +165 -165
  20. package/database/mongodb.js +146 -146
  21. package/database/redis.js +201 -201
  22. package/docs/README.md +36 -36
  23. package/docs/ammo.md +362 -362
  24. package/docs/api-reference.md +490 -489
  25. package/docs/auto-docs.md +216 -215
  26. package/docs/cli.md +152 -152
  27. package/docs/configuration.md +275 -233
  28. package/docs/database.md +390 -391
  29. package/docs/error-handling.md +438 -417
  30. package/docs/file-uploads.md +333 -334
  31. package/docs/getting-started.md +214 -215
  32. package/docs/middleware.md +355 -356
  33. package/docs/rate-limiting.md +393 -394
  34. package/docs/routing.md +302 -302
  35. package/package.json +62 -62
  36. package/rate-limit/algorithms/fixed-window.js +141 -141
  37. package/rate-limit/algorithms/sliding-window.js +147 -147
  38. package/rate-limit/algorithms/token-bucket.js +115 -115
  39. package/rate-limit/base.js +165 -165
  40. package/rate-limit/index.js +147 -147
  41. package/rate-limit/storage/base.js +104 -104
  42. package/rate-limit/storage/memory.js +101 -101
  43. package/rate-limit/storage/redis.js +88 -88
  44. package/server/ammo/body-parser.js +220 -220
  45. package/server/ammo/dispatch-helper.js +103 -103
  46. package/server/ammo/enhancer.js +57 -57
  47. package/server/ammo.js +454 -356
  48. package/server/endpoint.js +97 -74
  49. package/server/error.js +9 -9
  50. package/server/errors/code-context.js +125 -0
  51. package/server/errors/llm-error-service.js +140 -0
  52. package/server/files/helper.js +33 -33
  53. package/server/files/uploader.js +143 -143
  54. package/server/handler.js +158 -113
  55. package/server/target.js +185 -175
  56. package/server/targets/middleware-validator.js +22 -22
  57. package/server/targets/path-validator.js +21 -21
  58. package/server/targets/registry.js +160 -160
  59. package/server/targets/shoot-validator.js +21 -21
  60. package/te.js +428 -363
  61. package/utils/auto-register.js +17 -17
  62. package/utils/configuration.js +64 -64
  63. package/utils/errors-llm-config.js +84 -0
  64. package/utils/request-logger.js +43 -43
  65. package/utils/status-codes.js +82 -82
  66. package/utils/tejas-entrypoint-html.js +18 -18
  67. package/auto-docs/llm/index.js +0 -6
  68. package/auto-docs/llm/parse.js +0 -88
@@ -1,417 +1,438 @@
1
- # Error Handling
2
-
3
- Tejas provides robust error handling to keep your application running even when unexpected errors occur.
4
-
5
- ## Zero-Config Error Handling
6
-
7
- **One of Tejas's most powerful features is that you don't need to write any error handling code** — the framework catches all errors automatically at multiple levels.
8
-
9
- ### How It Works
10
-
11
- Tejas wraps all middleware and route handlers with built-in error catching. Any error thrown in your code is automatically:
12
-
13
- 1. **Caught** by the framework's error handler
14
- 2. **Logged** (if exception logging is enabled)
15
- 3. **Converted** to an appropriate HTTP error response
16
-
17
- This means your application **never crashes** from unhandled exceptions, and clients always receive proper error responses.
18
-
19
- ### Write Clean Code Without Try-Catch
20
-
21
- ```javascript
22
- // ✅ No try-catch needed — Tejas handles errors automatically
23
- target.register('/users/:id', async (ammo) => {
24
- const user = await database.findUser(ammo.payload.id); // If this throws, Tejas catches it
25
- const posts = await database.getUserPosts(user.id); // Same here
26
- ammo.fire({ user, posts });
27
- });
28
- ```
29
-
30
- Compare this to traditional frameworks where you'd need:
31
-
32
- ```javascript
33
- // Traditional approach requires manual error handling
34
- app.get('/users/:id', async (req, res) => {
35
- try {
36
- const user = await database.findUser(req.params.id);
37
- const posts = await database.getUserPosts(user.id);
38
- res.json({ user, posts });
39
- } catch (error) {
40
- console.error(error);
41
- res.status(500).json({ error: 'Internal Server Error' });
42
- }
43
- });
44
- ```
45
-
46
- ### Automatic Error Responses
47
-
48
- When an unhandled error occurs, Tejas automatically sends a `500 Internal Server Error` response. For intentional errors using `TejError`, the appropriate status code is used.
49
-
50
- ### Enable Error Logging
51
-
52
- To see caught exceptions in your logs, enable exception logging:
53
-
54
- ```javascript
55
- const app = new Tejas({
56
- log: {
57
- exceptions: true // Log all caught exceptions
58
- }
59
- });
60
- ```
61
-
62
- Or via environment variable:
63
- ```bash
64
- LOG_EXCEPTIONS=true
65
- ```
66
-
67
- ---
68
-
69
- ## TejError Class
70
-
71
- Use `TejError` for throwing HTTP errors with status codes:
72
-
73
- ```javascript
74
- import { TejError } from 'te.js';
75
-
76
- // Throw a 404 error
77
- throw new TejError(404, 'User not found');
78
-
79
- // Throw a 400 error
80
- throw new TejError(400, 'Invalid email format');
81
-
82
- // Throw a 500 error
83
- throw new TejError(500, 'Database connection failed');
84
- ```
85
-
86
- ## Error Response
87
-
88
- When an error is thrown, Tejas automatically sends the appropriate HTTP response:
89
-
90
- ```javascript
91
- throw new TejError(404, 'Resource not found');
92
- ```
93
-
94
- **Response:**
95
- ```
96
- HTTP/1.1 404 Not Found
97
- Content-Type: text/plain
98
-
99
- Resource not found
100
- ```
101
-
102
- ## Convenience Methods
103
-
104
- `Ammo` provides shortcut methods for common errors:
105
-
106
- ```javascript
107
- // 404 Not Found
108
- ammo.notFound();
109
-
110
- // 405 Method Not Allowed
111
- ammo.notAllowed();
112
-
113
- // 401 Unauthorized
114
- ammo.unauthorized();
115
- ```
116
-
117
- ## Using ammo.throw()
118
-
119
- For more control, use `ammo.throw()`:
120
-
121
- ```javascript
122
- // Just status code (uses default message)
123
- ammo.throw(404);
124
-
125
- // Status code with message
126
- ammo.throw(404, 'User not found');
127
-
128
- // Error object
129
- ammo.throw(new Error('Something went wrong'));
130
-
131
- // TejError
132
- ammo.throw(new TejError(400, 'Bad request'));
133
- ```
134
-
135
- ## Error Handling in Routes
136
-
137
- ### Basic Pattern
138
-
139
- ```javascript
140
- target.register('/users/:id', async (ammo) => {
141
- const { id } = ammo.payload;
142
-
143
- const user = await findUser(id);
144
-
145
- if (!user) {
146
- throw new TejError(404, 'User not found');
147
- }
148
-
149
- ammo.fire(user);
150
- });
151
- ```
152
-
153
- ### Try-Catch Pattern
154
-
155
- ```javascript
156
- target.register('/data', async (ammo) => {
157
- try {
158
- const data = await riskyOperation();
159
- ammo.fire(data);
160
- } catch (error) {
161
- if (error.code === 'TIMEOUT') {
162
- throw new TejError(504, 'Gateway timeout');
163
- }
164
- throw new TejError(500, 'Internal server error');
165
- }
166
- });
167
- ```
168
-
169
- ## Global Error Handling
170
-
171
- Errors are automatically caught by Tejas's handler. Enable logging:
172
-
173
- ```javascript
174
- const app = new Tejas({
175
- log: {
176
- exceptions: true // Log all exceptions
177
- }
178
- });
179
- ```
180
-
181
- ## Custom Error Middleware
182
-
183
- Create middleware to customize error handling:
184
-
185
- ```javascript
186
- // middleware/error-handler.js
187
- export const errorHandler = (ammo, next) => {
188
- const originalThrow = ammo.throw.bind(ammo);
189
-
190
- ammo.throw = (...args) => {
191
- // Log errors
192
- console.error('Error:', args);
193
-
194
- // Send to error tracking service
195
- errorTracker.capture(args[0]);
196
-
197
- // Call original throw
198
- originalThrow(...args);
199
- };
200
-
201
- next();
202
- };
203
-
204
- // Apply globally
205
- app.midair(errorHandler);
206
- ```
207
-
208
- ## Structured Error Responses
209
-
210
- For APIs, return structured error objects:
211
-
212
- ```javascript
213
- // middleware/api-errors.js
214
- export const apiErrorHandler = (ammo, next) => {
215
- const originalThrow = ammo.throw.bind(ammo);
216
-
217
- ammo.throw = (statusOrError, message) => {
218
- let status = 500;
219
- let errorMessage = 'Internal Server Error';
220
- let errorCode = 'INTERNAL_ERROR';
221
-
222
- if (typeof statusOrError === 'number') {
223
- status = statusOrError;
224
- errorMessage = message || getDefaultMessage(status);
225
- errorCode = getErrorCode(status);
226
- } else if (statusOrError instanceof TejError) {
227
- status = statusOrError.code;
228
- errorMessage = statusOrError.message;
229
- errorCode = getErrorCode(status);
230
- }
231
-
232
- ammo.fire(status, {
233
- error: {
234
- code: errorCode,
235
- message: errorMessage,
236
- status
237
- }
238
- });
239
- };
240
-
241
- next();
242
- };
243
-
244
- function getDefaultMessage(status) {
245
- const messages = {
246
- 400: 'Bad Request',
247
- 401: 'Unauthorized',
248
- 403: 'Forbidden',
249
- 404: 'Not Found',
250
- 405: 'Method Not Allowed',
251
- 429: 'Too Many Requests',
252
- 500: 'Internal Server Error'
253
- };
254
- return messages[status] || 'Unknown Error';
255
- }
256
-
257
- function getErrorCode(status) {
258
- const codes = {
259
- 400: 'BAD_REQUEST',
260
- 401: 'UNAUTHORIZED',
261
- 403: 'FORBIDDEN',
262
- 404: 'NOT_FOUND',
263
- 405: 'METHOD_NOT_ALLOWED',
264
- 429: 'RATE_LIMITED',
265
- 500: 'INTERNAL_ERROR'
266
- };
267
- return codes[status] || 'UNKNOWN_ERROR';
268
- }
269
- ```
270
-
271
- **Response:**
272
- ```json
273
- {
274
- "error": {
275
- "code": "NOT_FOUND",
276
- "message": "User not found",
277
- "status": 404
278
- }
279
- }
280
- ```
281
-
282
- ## Validation Errors
283
-
284
- For input validation, return detailed errors:
285
-
286
- ```javascript
287
- target.register('/users', (ammo) => {
288
- if (!ammo.POST) return ammo.notAllowed();
289
-
290
- const { name, email, age } = ammo.payload;
291
- const errors = [];
292
-
293
- if (!name) errors.push({ field: 'name', message: 'Name is required' });
294
- if (!email) errors.push({ field: 'email', message: 'Email is required' });
295
- if (email && !isValidEmail(email)) {
296
- errors.push({ field: 'email', message: 'Invalid email format' });
297
- }
298
- if (age && (isNaN(age) || age < 0)) {
299
- errors.push({ field: 'age', message: 'Age must be a positive number' });
300
- }
301
-
302
- if (errors.length > 0) {
303
- return ammo.fire(400, {
304
- error: {
305
- code: 'VALIDATION_ERROR',
306
- message: 'Validation failed',
307
- details: errors
308
- }
309
- });
310
- }
311
-
312
- // Process valid data...
313
- });
314
- ```
315
-
316
- ## Async Error Handling
317
-
318
- Tejas automatically catches errors in **both sync and async handlers** — including Promise rejections:
319
-
320
- ```javascript
321
- // ✅ No try-catch needed — errors are caught automatically
322
- target.register('/async', async (ammo) => {
323
- const data = await fetchData(); // If this throws, Tejas catches it
324
- ammo.fire(data);
325
- });
326
-
327
- // Multiple await calls? Still no try-catch needed
328
- target.register('/complex', async (ammo) => {
329
- const user = await getUser(ammo.payload.id);
330
- const profile = await getProfile(user.profileId);
331
- const settings = await getSettings(user.id);
332
- ammo.fire({ user, profile, settings });
333
- });
334
-
335
- // 🔧 Use try-catch ONLY when you need custom error handling
336
- target.register('/async-custom', async (ammo) => {
337
- try {
338
- const data = await fetchData();
339
- ammo.fire(data);
340
- } catch (error) {
341
- if (error.code === 'ECONNREFUSED') {
342
- throw new TejError(503, 'Service temporarily unavailable');
343
- }
344
- throw error; // Re-throw unknown errors (Tejas will still catch it)
345
- }
346
- });
347
- ```
348
-
349
- ### When You Still Might Want Try-Catch
350
-
351
- While Tejas catches all errors automatically, you may want try-catch for:
352
-
353
- 1. **Custom error mapping** — Convert database errors to user-friendly messages
354
- 2. **Retry logic** — Attempt an operation multiple times before failing
355
- 3. **Cleanup operations** — Release resources even on failure
356
- 4. **Partial success** Continue processing after non-critical failures
357
-
358
- ## BodyParserError
359
-
360
- `BodyParserError` is a subclass of `TejError` thrown automatically during request body parsing. You do not need to handle these yourself — they are caught by the framework and converted to appropriate HTTP responses.
361
-
362
- | Status | Condition |
363
- |--------|-----------|
364
- | **400** | Malformed JSON, invalid URL-encoded data, or corrupted multipart form data |
365
- | **408** | Body parsing timed out (exceeds `body.timeout`, default 30 seconds) |
366
- | **413** | Request body exceeds `body.max_size` (default 10 MB) |
367
- | **415** | Unsupported content type (not JSON, URL-encoded, or multipart) |
368
-
369
- These limits are configured via [Configuration](./configuration.md) (`body.max_size`, `body.timeout`).
370
-
371
- Supported content types:
372
- - `application/json`
373
- - `application/x-www-form-urlencoded`
374
- - `multipart/form-data`
375
-
376
- ## Error Flow
377
-
378
- When any error occurs in your handler or middleware, this is what happens internally:
379
-
380
- 1. The framework's `executeChain()` catches the error
381
- 2. If `LOG_EXCEPTIONS` is enabled, the error is logged
382
- 3. The error is passed to `ammo.throw()`:
383
- - **TejError** uses the error's `code` and `message` directly
384
- - **Standard Error** — returns 500 with the error message
385
- - **Anything else** returns 500 with a string representation
386
- 4. `ammo.throw()` calls `ammo.fire(statusCode, message)` to send the HTTP response
387
-
388
- Once a response has been sent (`res.headersSent` is true), no further middleware or handlers execute.
389
-
390
- ## Error Codes Reference
391
-
392
- | Status | Name | When to Use |
393
- |--------|------|-------------|
394
- | 400 | Bad Request | Invalid input, malformed request |
395
- | 401 | Unauthorized | Missing or invalid authentication |
396
- | 403 | Forbidden | Authenticated but not authorized |
397
- | 404 | Not Found | Resource doesn't exist |
398
- | 405 | Method Not Allowed | HTTP method not supported |
399
- | 409 | Conflict | Resource conflict (duplicate) |
400
- | 413 | Payload Too Large | Request body too large |
401
- | 422 | Unprocessable Entity | Valid syntax but semantic errors |
402
- | 429 | Too Many Requests | Rate limit exceeded |
403
- | 500 | Internal Server Error | Unexpected server errors |
404
- | 502 | Bad Gateway | Upstream server error |
405
- | 503 | Service Unavailable | Server temporarily unavailable |
406
- | 504 | Gateway Timeout | Upstream server timeout |
407
-
408
- ## Best Practices
409
-
410
- 1. **Use appropriate status codes** — Don't return 500 for client errors
411
- 2. **Provide useful messages** — Help developers debug issues
412
- 3. **Don't expose internals** — Hide stack traces in production
413
- 4. **Log errors** Enable exception logging for debugging
414
- 5. **Be consistent** — Use the same error format throughout your API
415
- 6. **Validate early** Check input before processing
416
- 7. **Use TejError** For HTTP-specific errors with status codes
417
-
1
+ # Error Handling
2
+
3
+ Tejas keeps your application from crashing on unhandled errors. You don't log the error and send the response separately **`ammo.throw()` is the single mechanism**: it sends the appropriate HTTP response (logging is optional via `log.exceptions`). Whether you call `ammo.throw()` or the framework calls it when it catches an error, the same behaviour applies. When LLM-inferred errors are enabled, call `ammo.throw()` with no arguments and an LLM infers status and message from code context; explicit code and message always override.
4
+
5
+ ## Zero-Config Error Handling
6
+
7
+ **One of Tejas's most powerful features is that you don't need to write any error handling code** — the framework catches all errors automatically at multiple levels.
8
+
9
+ ### How It Works
10
+
11
+ Tejas wraps all middleware and route handlers with built-in error catching. Any error thrown in your code is automatically passed to `ammo.throw(err)` — the same mechanism you use for intentional errors. So: one place handles everything (response + optional logging via `log.exceptions`). No separate "log then send response"; your app never crashes and clients always receive a proper response.
12
+
13
+ ### Write Clean Code Without Try-Catch
14
+
15
+ ```javascript
16
+ // ✅ No try-catch needed — Tejas handles errors automatically
17
+ target.register('/users/:id', async (ammo) => {
18
+ const user = await database.findUser(ammo.payload.id); // If this throws, Tejas catches it
19
+ const posts = await database.getUserPosts(user.id); // Same here
20
+ ammo.fire({ user, posts });
21
+ });
22
+ ```
23
+
24
+ In other frameworks you typically **log the error and then send the response** (two separate steps). With Tejas, **`ammo.throw()` does both** and when the framework catches an error it uses the same `ammo.throw()`, so you never define them separately:
25
+
26
+ ```javascript
27
+ // ❌ Traditional: log then send response (two separate things)
28
+ app.get('/users/:id', async (req, res) => {
29
+ try {
30
+ const user = await database.findUser(req.params.id);
31
+ res.json(user);
32
+ } catch (error) {
33
+ console.error(error); // 1. log
34
+ res.status(500).json({ error: 'Internal Server Error' }); // 2. send response
35
+ }
36
+ });
37
+ ```
38
+
39
+ ### Automatic Error Responses
40
+
41
+ When an unhandled error occurs, the framework calls `ammo.throw(err)` — the same method you use for intentional errors. So one mechanism: explicit `ammo.throw()` or framework-caught, both go through `ammo.throw()`. When [LLM-inferred errors](#llm-inferred-errors) are enabled, status and message are inferred from code context; otherwise or when you pass explicit code/message, those are used.
42
+
43
+ ### Enable Error Logging
44
+
45
+ To see caught exceptions in your logs, enable exception logging:
46
+
47
+ ```javascript
48
+ const app = new Tejas({
49
+ log: {
50
+ exceptions: true // Log all caught exceptions
51
+ }
52
+ });
53
+ ```
54
+
55
+ Or via environment variable:
56
+
57
+ ```bash
58
+ LOG_EXCEPTIONS=true
59
+ ```
60
+
61
+ ---
62
+
63
+ ## LLM-Inferred Errors
64
+
65
+ When **`errors.llm.enabled`** is true and you call `ammo.throw()` without an explicit status code or message, Tejas uses an LLM to infer an appropriate HTTP status code and message from **code context** — you do not pass an error object. The framework captures the code surrounding the `ammo.throw()` call (with line numbers) and all **upstream** (callers) and **downstream** (code that would have run next) context, and the LLM infers what went wrong from that. Explicit code and message always override.
66
+
67
+ - **No error object required:** Call `ammo.throw()` with no arguments (or only options). The LLM receives the source code around the call site and upstream call stacks so it can infer status and message from control flow and intent.
68
+ - **Opt-in:** Enable via config: `errors.llm.enabled: true` and configure `errors.llm` (baseURL, apiKey, model), or call **`app.withLLMErrors()`** / **`app.withLLMErrors({ baseURL, apiKey, model, messageType })`** before `takeoff()`. See [Configuration](./configuration.md#error-handling-llm-inferred-errors).
69
+ - **Framework-caught errors:** When the framework catches an unhandled error (in a handler or middleware), it uses the same `ammo.throw(err)` — so the same `errors.llm` config applies. No separate "log then send response"; one mechanism handles everything.
70
+ - **Override:** Whenever you pass a status code or message (e.g. `ammo.throw(404, 'User not found')` or `throw new TejError(404, 'User not found')`), that value is used; the LLM is not called.
71
+ - **Message type:** Configure whether the LLM generates **end-user-friendly** or **developer-friendly** messages via `errors.llm.messageType`; override per call (see [Per-call overrides](#per-call-overrides)).
72
+ - **Non-production:** In non-production, the LLM can also provide developer insight (e.g. bug vs environment, suggested fix), attached to the response as `_dev` or in logs only — never in production.
73
+
74
+ ### Per-call overrides
75
+
76
+ For any LLM-eligible `ammo.throw()` call (no explicit status code), you can pass an options object as the last argument to override behaviour for that call only:
77
+
78
+ - **`useLlm`** (boolean): Set to `false` to skip the LLM for this call and respond with a default 500 / "Internal Server Error" (or the error's message when you pass an Error/string). Set to `true` to force using the LLM (same as default when eligible).
79
+ - **`messageType`** (`"endUser"` | `"developer"`): Override the configured default for this call — request an end-user-friendly or developer-friendly message.
80
+
81
+ ```javascript
82
+ // Skip LLM for this call; send 500
83
+ ammo.throw({ useLlm: false });
84
+
85
+ // Request a developer-friendly message for this call only
86
+ ammo.throw({ messageType: 'developer' });
87
+
88
+ // When an error was caught and passed in, you can still pass options
89
+ ammo.throw(caughtErr, { useLlm: false });
90
+ ```
91
+
92
+ ---
93
+
94
+ ## TejError Class
95
+
96
+ Use `TejError` for throwing HTTP errors with status codes. Both status code and message are **optional** when [LLM-inferred errors](#llm-inferred-errors) are enabled and the error is passed through `ammo.throw()`; otherwise, supply them to set the response explicitly.
97
+
98
+ ```javascript
99
+ import { TejError } from 'te.js';
100
+
101
+ // Explicit code and message (always used as override)
102
+ throw new TejError(404, 'User not found');
103
+ throw new TejError(400, 'Invalid email format');
104
+ throw new TejError(500, 'Database connection failed');
105
+ ```
106
+
107
+ ## Error Response
108
+
109
+ When an error is thrown, Tejas automatically sends the appropriate HTTP response:
110
+
111
+ ```javascript
112
+ throw new TejError(404, 'Resource not found');
113
+ ```
114
+
115
+ **Response:**
116
+ ```
117
+ HTTP/1.1 404 Not Found
118
+ Content-Type: text/plain
119
+
120
+ Resource not found
121
+ ```
122
+
123
+ ## Convenience Methods
124
+
125
+ `Ammo` provides shortcut methods for common errors:
126
+
127
+ ```javascript
128
+ // 404 Not Found
129
+ ammo.notFound();
130
+
131
+ // 405 Method Not Allowed
132
+ ammo.notAllowed();
133
+
134
+ // 401 Unauthorized
135
+ ammo.unauthorized();
136
+ ```
137
+
138
+ ## Using ammo.throw()
139
+
140
+ For more control, use `ammo.throw()`. When [LLM-inferred errors](#llm-inferred-errors) are enabled, you can omit code and message and the LLM will infer them; otherwise, or when you want to override, pass them explicitly.
141
+
142
+ ```javascript
143
+ // Explicit: status code and/or message
144
+ ammo.throw(404);
145
+ ammo.throw(404, 'User not found');
146
+ ammo.throw(new TejError(400, 'Bad request'));
147
+
148
+ // When errors.llm.enabled: LLM infers code and message from context
149
+ ammo.throw(new Error('Something went wrong'));
150
+ ammo.throw('Validation failed');
151
+ ammo.throw(); // context still used when available
152
+ ```
153
+
154
+ See [Ammo — throw()](./ammo.md#throw--send-error-response) for all signatures and the LLM-inferred row.
155
+
156
+ ## Error Handling in Routes
157
+
158
+ ### Basic Pattern
159
+
160
+ ```javascript
161
+ target.register('/users/:id', async (ammo) => {
162
+ const { id } = ammo.payload;
163
+
164
+ const user = await findUser(id);
165
+
166
+ if (!user) {
167
+ throw new TejError(404, 'User not found');
168
+ }
169
+
170
+ ammo.fire(user);
171
+ });
172
+ ```
173
+
174
+ ### Try-Catch Pattern
175
+
176
+ ```javascript
177
+ target.register('/data', async (ammo) => {
178
+ try {
179
+ const data = await riskyOperation();
180
+ ammo.fire(data);
181
+ } catch (error) {
182
+ if (error.code === 'TIMEOUT') {
183
+ throw new TejError(504, 'Gateway timeout');
184
+ }
185
+ throw new TejError(500, 'Internal server error');
186
+ }
187
+ });
188
+ ```
189
+
190
+ ## Global Error Handling
191
+
192
+ Errors are automatically caught by Tejas's handler. Enable logging:
193
+
194
+ ```javascript
195
+ const app = new Tejas({
196
+ log: {
197
+ exceptions: true // Log all exceptions
198
+ }
199
+ });
200
+ ```
201
+
202
+ ## Custom Error Middleware
203
+
204
+ Create middleware to customize error handling:
205
+
206
+ ```javascript
207
+ // middleware/error-handler.js
208
+ export const errorHandler = (ammo, next) => {
209
+ const originalThrow = ammo.throw.bind(ammo);
210
+
211
+ ammo.throw = (...args) => {
212
+ // Log errors
213
+ console.error('Error:', args);
214
+
215
+ // Send to error tracking service
216
+ errorTracker.capture(args[0]);
217
+
218
+ // Call original throw
219
+ originalThrow(...args);
220
+ };
221
+
222
+ next();
223
+ };
224
+
225
+ // Apply globally
226
+ app.midair(errorHandler);
227
+ ```
228
+
229
+ ## Structured Error Responses
230
+
231
+ For APIs, return structured error objects:
232
+
233
+ ```javascript
234
+ // middleware/api-errors.js
235
+ export const apiErrorHandler = (ammo, next) => {
236
+ const originalThrow = ammo.throw.bind(ammo);
237
+
238
+ ammo.throw = (statusOrError, message) => {
239
+ let status = 500;
240
+ let errorMessage = 'Internal Server Error';
241
+ let errorCode = 'INTERNAL_ERROR';
242
+
243
+ if (typeof statusOrError === 'number') {
244
+ status = statusOrError;
245
+ errorMessage = message || getDefaultMessage(status);
246
+ errorCode = getErrorCode(status);
247
+ } else if (statusOrError instanceof TejError) {
248
+ status = statusOrError.code;
249
+ errorMessage = statusOrError.message;
250
+ errorCode = getErrorCode(status);
251
+ }
252
+
253
+ ammo.fire(status, {
254
+ error: {
255
+ code: errorCode,
256
+ message: errorMessage,
257
+ status
258
+ }
259
+ });
260
+ };
261
+
262
+ next();
263
+ };
264
+
265
+ function getDefaultMessage(status) {
266
+ const messages = {
267
+ 400: 'Bad Request',
268
+ 401: 'Unauthorized',
269
+ 403: 'Forbidden',
270
+ 404: 'Not Found',
271
+ 405: 'Method Not Allowed',
272
+ 429: 'Too Many Requests',
273
+ 500: 'Internal Server Error'
274
+ };
275
+ return messages[status] || 'Unknown Error';
276
+ }
277
+
278
+ function getErrorCode(status) {
279
+ const codes = {
280
+ 400: 'BAD_REQUEST',
281
+ 401: 'UNAUTHORIZED',
282
+ 403: 'FORBIDDEN',
283
+ 404: 'NOT_FOUND',
284
+ 405: 'METHOD_NOT_ALLOWED',
285
+ 429: 'RATE_LIMITED',
286
+ 500: 'INTERNAL_ERROR'
287
+ };
288
+ return codes[status] || 'UNKNOWN_ERROR';
289
+ }
290
+ ```
291
+
292
+ **Response:**
293
+ ```json
294
+ {
295
+ "error": {
296
+ "code": "NOT_FOUND",
297
+ "message": "User not found",
298
+ "status": 404
299
+ }
300
+ }
301
+ ```
302
+
303
+ ## Validation Errors
304
+
305
+ For input validation, return detailed errors:
306
+
307
+ ```javascript
308
+ target.register('/users', (ammo) => {
309
+ if (!ammo.POST) return ammo.notAllowed();
310
+
311
+ const { name, email, age } = ammo.payload;
312
+ const errors = [];
313
+
314
+ if (!name) errors.push({ field: 'name', message: 'Name is required' });
315
+ if (!email) errors.push({ field: 'email', message: 'Email is required' });
316
+ if (email && !isValidEmail(email)) {
317
+ errors.push({ field: 'email', message: 'Invalid email format' });
318
+ }
319
+ if (age && (isNaN(age) || age < 0)) {
320
+ errors.push({ field: 'age', message: 'Age must be a positive number' });
321
+ }
322
+
323
+ if (errors.length > 0) {
324
+ return ammo.fire(400, {
325
+ error: {
326
+ code: 'VALIDATION_ERROR',
327
+ message: 'Validation failed',
328
+ details: errors
329
+ }
330
+ });
331
+ }
332
+
333
+ // Process valid data...
334
+ });
335
+ ```
336
+
337
+ ## Async Error Handling
338
+
339
+ Tejas automatically catches errors in **both sync and async handlers** — including Promise rejections:
340
+
341
+ ```javascript
342
+ // No try-catch needed — errors are caught automatically
343
+ target.register('/async', async (ammo) => {
344
+ const data = await fetchData(); // If this throws, Tejas catches it
345
+ ammo.fire(data);
346
+ });
347
+
348
+ // ✅ Multiple await calls? Still no try-catch needed
349
+ target.register('/complex', async (ammo) => {
350
+ const user = await getUser(ammo.payload.id);
351
+ const profile = await getProfile(user.profileId);
352
+ const settings = await getSettings(user.id);
353
+ ammo.fire({ user, profile, settings });
354
+ });
355
+
356
+ // 🔧 Use try-catch ONLY when you need custom error handling
357
+ target.register('/async-custom', async (ammo) => {
358
+ try {
359
+ const data = await fetchData();
360
+ ammo.fire(data);
361
+ } catch (error) {
362
+ if (error.code === 'ECONNREFUSED') {
363
+ throw new TejError(503, 'Service temporarily unavailable');
364
+ }
365
+ throw error; // Re-throw unknown errors (Tejas will still catch it)
366
+ }
367
+ });
368
+ ```
369
+
370
+ ### When You Still Might Want Try-Catch
371
+
372
+ While Tejas catches all errors automatically, you may want try-catch for:
373
+
374
+ 1. **Custom error mapping** — Convert database errors to user-friendly messages
375
+ 2. **Retry logic** — Attempt an operation multiple times before failing
376
+ 3. **Cleanup operations** — Release resources even on failure
377
+ 4. **Partial success** — Continue processing after non-critical failures
378
+
379
+ ## BodyParserError
380
+
381
+ `BodyParserError` is a subclass of `TejError` thrown automatically during request body parsing. You do not need to handle these yourself — they are caught by the framework and converted to appropriate HTTP responses.
382
+
383
+ | Status | Condition |
384
+ |--------|-----------|
385
+ | **400** | Malformed JSON, invalid URL-encoded data, or corrupted multipart form data |
386
+ | **408** | Body parsing timed out (exceeds `body.timeout`, default 30 seconds) |
387
+ | **413** | Request body exceeds `body.max_size` (default 10 MB) |
388
+ | **415** | Unsupported content type (not JSON, URL-encoded, or multipart) |
389
+
390
+ These limits are configured via [Configuration](./configuration.md) (`body.max_size`, `body.timeout`).
391
+
392
+ Supported content types:
393
+ - `application/json`
394
+ - `application/x-www-form-urlencoded`
395
+ - `multipart/form-data`
396
+
397
+ ## Error Flow
398
+
399
+ When any error occurs in your handler or middleware, the framework uses the **same** `ammo.throw(err)` you use for intentional errors — one mechanism:
400
+
401
+ 1. The framework's `executeChain()` catches the error
402
+ 2. If `LOG_EXCEPTIONS` is enabled, the error is logged
403
+ 3. The error is passed to `ammo.throw(err)` (no separate "send response" step — `ammo.throw()` does it)
404
+ 4. **TejError** uses the error's `code` and `message` directly
405
+ 5. **When errors.llm.enabled** LLM infers status and message from code context (same as explicit `ammo.throw()`)
406
+ 6. **Otherwise** 500 with the error message or string representation
407
+ 7. `ammo.throw()` sends the HTTP response via `ammo.fire(statusCode, message)`
408
+
409
+ Once a response has been sent (`res.headersSent` is true), no further middleware or handlers execute.
410
+
411
+ ## Error Codes Reference
412
+
413
+ | Status | Name | When to Use |
414
+ |--------|------|-------------|
415
+ | 400 | Bad Request | Invalid input, malformed request |
416
+ | 401 | Unauthorized | Missing or invalid authentication |
417
+ | 403 | Forbidden | Authenticated but not authorized |
418
+ | 404 | Not Found | Resource doesn't exist |
419
+ | 405 | Method Not Allowed | HTTP method not supported |
420
+ | 409 | Conflict | Resource conflict (duplicate) |
421
+ | 413 | Payload Too Large | Request body too large |
422
+ | 422 | Unprocessable Entity | Valid syntax but semantic errors |
423
+ | 429 | Too Many Requests | Rate limit exceeded |
424
+ | 500 | Internal Server Error | Unexpected server errors |
425
+ | 502 | Bad Gateway | Upstream server error |
426
+ | 503 | Service Unavailable | Server temporarily unavailable |
427
+ | 504 | Gateway Timeout | Upstream server timeout |
428
+
429
+ ## Best Practices
430
+
431
+ 1. **Use appropriate status codes** — Don't return 500 for client errors
432
+ 2. **Provide useful messages** — Help developers debug issues
433
+ 3. **Don't expose internals** — Hide stack traces in production
434
+ 4. **Log errors** — Enable exception logging for debugging
435
+ 5. **Be consistent** — Use the same error format throughout your API
436
+ 6. **Validate early** — Check input before processing
437
+ 7. **Use TejError or ammo.throw(code, message)** — For HTTP-specific errors when you want explicit control
438
+ 8. **Opt in to LLM-inferred errors when helpful** — Enable via `errors.llm.enabled` in config or **`app.withLLMErrors(config?)`** before `takeoff()`, then configure baseURL, apiKey, and model so you can throw without specifying code or message and let the LLM infer them; see [Configuration](./configuration.md#error-handling-llm-inferred-errors)