Documentation
Error codes
Every error Kynetra FX produces is typed, structured, and machine-readable. This page documents all built-in FX_ error codes, the JSON shape of error responses, and how to throw or intercept errors in your own handlers.
Error contract#
Whenever a Kynetra FX application returns an error — whether from a built-in guard or from an FXError thrown in your handler — the response body follows a single consistent shape. The HTTP status code always matches the error.code semantically, so clients can branch on either the numeric status or the string code.
The FXError class is the canonical way to signal a known, expected failure from inside a handler or middleware. It carries a numeric HTTP status, an FX_ code string, a human-readable message, and an optional details payload for structured context (such as validation issues).
Note
FXError instances are caught by the runtime and re-emitted as FX_INTERNAL_ERROR (500). The original error is forwarded to your onError hook so you can log it before responding.Error response format#
All error responses, regardless of source, use the following envelope. The details field is optional and only present when extra structured context is available (for example, validation failure locations).
{ "error": { "code": "FX_VALIDATION_ERROR", "message": "Request validation failed", "details": { "issues": [ { "location": "body", "path": ["email"], "message": "Expected string" } ] } }}The top-level error key is always present. Successful responses never include this key, so clients can use its presence as a reliable error signal without inspecting the HTTP status.
All error codes#
The table below lists every built-in FX_ error code, the HTTP status it maps to, and a description of what triggers it. Codes in the 5xx range indicate a problem with your application configuration; codes in the 4xx range indicate a problem with the incoming request.
Note
pnpm check:error-docs verifies this table against the exported FX_ERROR_CODES and FX_ERROR_STATUS catalog.| Name | Type | Description |
|---|---|---|
| FX_NOT_FOUND | 404 | No route matched the request path and method. The router exhausted all registered patterns without a match. |
| FX_INTERNAL_ERROR | 500 | An unhandled exception was thrown inside a route handler or middleware and was not caught before reaching the error boundary. |
| FX_VALIDATION_ERROR | 422 | The request body or query string failed schema validation. The details.issues array contains per-field failure locations. |
| FX_UNAUTHENTICATED | 401 | Auth middleware required valid credentials but none were present or the provided token was invalid or expired. |
| FX_FORBIDDEN | 403 | The principal is authenticated but does not hold the required role or permission to access the resource. |
| FX_TENANT_REQUIRED | 400 | A route or middleware required a tenant ID (via header or subdomain) but none was present in the request. |
| FX_PLUGIN_MISSING_DEPENDENCY | 500 | A plugin declared a dependency that was not registered when the app booted. Registration order does not matter. |
| FX_WASM_ERROR | 502 | A WASM guest module threw an exception or returned a malformed response that could not be parsed as a valid WasmResponse. |
| FX_ROUTE_HANDLER_REQUIRED | 500 | app.route() was called without a handler function as the final argument. Every route definition must include a handler. |
| FX_NEXT_CALLED_MULTIPLE_TIMES | 500 | Middleware called next() more than once in the same request lifecycle. Each middleware may only advance the pipeline once. |
| FX_NODE_INTERNAL_ERROR | 500 | The Node adapter failed while translating or serving a Web Standards request and returned its final JSON error response. |
Throwing errors manually#
Use FXError to signal known, expected failures from inside your handlers or middleware. The constructor accepts an HTTP status, an FX_ code string, a message, and an optional details object.
import { createFX, FX_ERROR_CODES, FXError } from '@kynetra/fx' const app = createFX() app.get('/items/:id', async (ctx) => { const item = await db.find(ctx.params.id) if (!item) { throw new FXError(404, FX_ERROR_CODES.NOT_FOUND, `Item ${ctx.params.id} not found`) } return ctx.json(item)})You can also pass an arbitrary details object as the fourth argument. This is useful when you want clients to receive structured context alongside the error — for instance, the conflicting resource ID, a retry-after timestamp, or a set of allowed values.
throw new FXError(409, 'APP_SLUG_CONFLICT', 'Slug already taken', { existingId: 'org_01HXZ', slug: ctx.body.slug,})Tip
FX_ code strings. Passing a custom code such as APP_PAYMENT_REQUIRED is valid — Kynetra FX will forward it verbatim in the error envelope. Reserve the FX_ prefix for codes that the framework itself emits.Catching errors with the onError hook#
Register an onError hook to intercept every unhandled error before it reaches the default error serializer. This is the right place to log errors, enrich responses with request IDs, or translate third-party exceptions into typed FXError instances.
app.hook('onError', (ctx, err) => { if (err instanceof FXError && err.status === 404) { return ctx.json( { error: { code: err.code, message: err.message } }, { status: 404 } ) } console.error(err) return ctx.json( { error: { code: 'FX_INTERNAL_ERROR', message: 'Something went wrong' } }, { status: 500 } )})The hook receives the request context and the raw error object. If the hook returns a Response, that response is sent to the client. If the hook throws or returns nothing, the runtime falls back to the default serializer.
Warning
onError. If the hook itself throws, the runtime has no further fallback and will produce a bare 500 with no body.Validation errors in depth#
When a request fails schema validation against a contract's input or query schema, Kynetra FX emits FX_VALIDATION_ERROR (422) and populates details.issues with a list of per-field failures. Each issue names the location (body or query), the path array pointing to the failing field, and a human-readable message.
// POST /users with body: { "age": "not-a-number" }// HTTP 422{ "error": { "code": "FX_VALIDATION_ERROR", "message": "Request validation failed", "details": { "issues": [ { "location": "body", "path": ["age"], "message": "Expected number, received string" } ] } }}Multiple issues can be present in a single response if several fields fail validation simultaneously — Kynetra FX never short-circuits at the first failure. Clients should iterate over details.issues and surface all of them.
Validation is driven by whichever schema library you attach to the contract. Because Kynetra FX uses the Standard Schema protocol, Zod, Valibot, ArkType, and the built-in fx schema all produce issues in the same normalized shape. See Validation for the full guide.
Per-error guidance#
FX_NOT_FOUND
Emitted by the router when no registered route matches the method and path of an incoming request. Common causes:
- Typo in the client URL or HTTP method.
- Route registered after the request was handled (late registration in a plugin).
- Group prefix mismatch — the route path includes the prefix but the group already adds it.
FX_UNAUTHENTICATED and FX_FORBIDDEN
These are semantically distinct: FX_UNAUTHENTICATED (401) means the request carried no recognizable identity; FX_FORBIDDEN (403) means the identity was recognized but lacks permission. See Authentication and RBAC for how to configure the auth() middleware and role guards.
FX_TENANT_REQUIRED
Thrown by the tenant() middleware when required: true and no tenant ID could be resolved from the request. See Multi-tenancy for resolver configuration.
FX_PLUGIN_MISSING_DEPENDENCY
Thrown at boot time when a plugin lists a dependency that has not been registered yet. Ensure that dependency plugins are passed to app.register() before the plugin that depends on them. See Plugins for the full plugin lifecycle.
FX_WASM_ERROR
Thrown when a WASM guest module traps (throws an internal exception) or returns a response JSON string that cannot be deserialized into a valid WasmResponse. See WASM guests for the host ABI contract and how to implement a compliant guest.
FX_NEXT_CALLED_MULTIPLE_TIMES
Middleware must call await next() exactly once per request. Calling it a second time — for example inside a catch block that re-runs the inner pipeline — is a programming error and is caught immediately at runtime. Restructure the middleware so that the next() call appears on exactly one code path.