Documentation
Error Handling
Kynetra FX catches all errors thrown from handlers and middleware and converts them to structured JSON responses. Throw FXError for expected errors. Register an onError hook to customise the response for any error.
The error response shape#
Every error response from Kynetra FX has this JSON shape:
{ "error": { "code": "FX_NOT_FOUND", "message": "No route matched GET /unknown", "details": { } // optional — present for validation errors }}The code is a stable string identifier useful for error handling in clients. The message is human-readable. The optional details carries structured information (e.g. validation issue paths).
FXError#
Throw new FXError(status, code, message, details?) from any handler or middleware to produce a structured error response. Kynetra FX catches it and serialises it into the standard shape above.
import { createFX, FXError } from '@kynetra/fx' const app = createFX() app.get('/users/:id', async (ctx) => { const user = await findUser(ctx.params.id) if (!user) { throw new FXError(404, 'USER_NOT_FOUND', 'User not found') } return ctx.json(user)}) app.post('/transfer', async (ctx) => { const { amount } = await ctx.jsonBody<{ amount: number }>() if (amount <= 0) { throw new FXError(422, 'INVALID_AMOUNT', 'Amount must be positive', { field: 'amount', min: 0, }) } return ctx.json({ transferred: amount })})FXError constructor#
| Name | Type | Description |
|---|---|---|
| status | number | HTTP status code for the response (e.g. 400, 401, 403, 404, 422, 500). |
| code | string | Machine-readable error code string. Use uppercase snake_case by convention. |
| message | string | Human-readable description of the error. |
| details | unknown (optional) | Optional structured detail object included in the error.details field. |
Built-in error codes#
Kynetra FX uses these reserved error codes internally. Your custom error codes should not clash with them:
| Name | Type | Description |
|---|---|---|
| FX_NOT_FOUND | 404 | No route matched the request path and method. |
| FX_INTERNAL_ERROR | 500 | An uncaught error was thrown inside a handler or middleware. |
| FX_VALIDATION_ERROR | 422 | The request body or query params failed schema validation. |
| FX_UNAUTHENTICATED | 401 | Authentication was required but not provided or invalid. |
| FX_FORBIDDEN | 403 | The authenticated principal lacks the required role or permission. |
| FX_TENANT_REQUIRED | 400 | A tenant ID was required but not found in the request. |
| FX_PLUGIN_MISSING_DEPENDENCY | 500 | A plugin declared a dependency that was not registered. |
| FX_WASM_ERROR | 502 | A WASM guest handler threw an error or returned an invalid response. |
| FX_ROUTE_HANDLER_REQUIRED | 500 | app.route() was called without a handler function. |
| FX_NEXT_CALLED_MULTIPLE_TIMES | 500 | Middleware called next() more than once in a single request. |
Validation errors#
When app.route() validation fails, Kynetra FX automatically returns a 422 with the FX_VALIDATION_ERROR code and a structured details object listing each issue:
{ "error": { "code": "FX_VALIDATION_ERROR", "message": "Request validation failed", "details": { "issues": [ { "location": "body", "path": ["email"], "message": "Required" }, { "location": "body", "path": ["age"], "message": "Expected number" } ] } }}The onError hook#
Register an onError hook to customise error responses, log to an external service, or translate framework errors to domain-specific codes.
import { createFX, FXError } from '@kynetra/fx' const app = createFX() app.hook('onError', (ctx, err) => { // Log all 5xx errors to your observability platform if (err instanceof FXError && err.status >= 500) { console.error('5xx error', { code: err.code, message: err.message, url: ctx.url.href }) } // Return a custom response for 404s if (err instanceof FXError && err.code === 'FX_NOT_FOUND') { return ctx.json({ error: { code: 'NOT_FOUND', message: 'The requested resource does not exist.' } }, { status: 404 }) } // Return nothing (undefined) to let Kynetra FX use its default error response})Note
Response from onError to replace the default error response. Return nothing (or undefined) to let the framework produce the standard error body.Customising the 404 response#
The onError hook is the right place to customise the response for unmatched routes:
app.hook('onError', (ctx, err) => { if (err instanceof FXError && err.code === 'FX_NOT_FOUND') { return ctx.json({ error: { code: 'NOT_FOUND', message: 'No endpoint at ' + ctx.url.pathname, }, }, { status: 404 }) }})Catching non-FXError exceptions#
If a handler throws a plain Error (or any non-FXError value), Kynetra FX wraps it in a 500 FX_INTERNAL_ERROR response. The original error is passed to the onError hook:
app.hook('onError', (ctx, err) => { if (!(err instanceof FXError)) { // err is the raw thrown value console.error('Unhandled exception:', err) // Optionally report to Sentry, Datadog, etc. } // Return nothing → default 500 response}) // This will be caught and converted to FX_INTERNAL_ERROR:app.get('/crash', () => { throw new Error('database connection refused')})Error handling in middleware#
Middleware can catch errors from inner handlers by wrapping await next() in a try/catch:
export const errorBoundary: FXMiddleware = async (ctx, next) => { try { return await next() } catch (err) { if (err instanceof FXError && err.status === 429) { // Handle rate limit errors with Retry-After header return ctx.json( { error: { code: err.code, message: err.message } }, { status: 429, headers: { 'Retry-After': '60' } } ) } throw err // Re-throw so onError hook handles it }}Best practices#
- Always throw
FXErrorfor expected errors (not found, forbidden, validation). Reserve plainErrorfor true programmer errors. - Use your own namespace prefix for custom error codes (e.g.
APP_USER_NOT_FOUND) to avoid collisions with framework codes. - Log 5xx errors in the
onErrorhook so no server-side exception is silently swallowed. - Do not include stack traces or internal details in the
messagefield in production — they leak implementation details.