Kynetra FX

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 response
{
"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.

app.ts
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#

NameTypeDescription
statusnumberHTTP status code for the response (e.g. 400, 401, 403, 404, 422, 500).
codestringMachine-readable error code string. Use uppercase snake_case by convention.
messagestringHuman-readable description of the error.
detailsunknown (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:

NameTypeDescription
FX_NOT_FOUND404No route matched the request path and method.
FX_INTERNAL_ERROR500An uncaught error was thrown inside a handler or middleware.
FX_VALIDATION_ERROR422The request body or query params failed schema validation.
FX_UNAUTHENTICATED401Authentication was required but not provided or invalid.
FX_FORBIDDEN403The authenticated principal lacks the required role or permission.
FX_TENANT_REQUIRED400A tenant ID was required but not found in the request.
FX_PLUGIN_MISSING_DEPENDENCY500A plugin declared a dependency that was not registered.
FX_WASM_ERROR502A WASM guest handler threw an error or returned an invalid response.
FX_ROUTE_HANDLER_REQUIRED500app.route() was called without a handler function.
FX_NEXT_CALLED_MULTIPLE_TIMES500Middleware 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:

validation error response
{
"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.

app.ts
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

Return a 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.ts
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.ts
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:

middleware/error-boundary.ts
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 FXError for expected errors (not found, forbidden, validation). Reserve plain Error for 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 onError hook so no server-side exception is silently swallowed.
  • Do not include stack traces or internal details in the message field in production — they leak implementation details.