Errors
The error hierarchy, its status codes, and what reaches the operator.
Throw one of these from an action, an adapter, or a custom route and FlowPanel
turns it into the right HTTP status with a message that is safe to show. Throw
anything else and the caller gets a generic 500 — the detail is logged, never
sent.
That split is the point of the hierarchy: safeMessage is the part you have
decided an operator may read.
FlowpanelError
The base class. Every other error extends it.
export class FlowpanelError { constructor(code: FlowpanelErrorCode, safeMessage: string); }| Property | Meaning |
|---|---|
code | Stable machine-readable tag, e.g. "validation_failed". |
safeMessage | Shown to the operator verbatim. Keep internals out of it. |
status | HTTP status from FLOWPANEL_ERROR_STATUS[code]. |
Construct it directly for a case the subclasses do not cover:
throw new FlowpanelError("conflict", "This order is already being processed");The subclasses
| Class | code | Status | Default message | Raised by FlowPanel? |
|---|---|---|---|---|
FlowpanelValidationError | validation_failed | 422 | Validation failed | yes |
FlowpanelAuthError | unauthenticated | 401 | Authentication required | yours to throw |
FlowpanelAccessError | forbidden | 403 | Forbidden | yes |
FlowpanelFieldAccessError | field_forbidden | 403 | Field "x" cannot be changed. | yes |
FlowpanelOperationDisabledError | operation_disabled | 403 | This operation is disabled. | yes |
FlowpanelUnknownFieldError | unknown_field | 400 | Unknown field: "x". | yes |
FlowpanelNotFoundError | not_found | 404 | Not found | yes |
FlowpanelConflictError | conflict | 409 | Conflict | no — yours to throw |
FlowpanelRateLimitError | rate_limited | 429 | Rate limit exceeded | yes |
Each takes an optional message overriding the default.
The last column matters when you write a hooks.onError or a log filter.
FlowPanel raises FlowpanelAccessError for rejected admin/resource access and
an unbound tenant scope, FlowpanelFieldAccessError for prohibited field
reads/writes, FlowpanelOperationDisabledError for disabled mutations,
FlowpanelUnknownFieldError for fields outside the resource contract,
FlowpanelRateLimitError from the request prologue, and the not-found and
validation errors from resource routes. FlowpanelAuthError and
FlowpanelConflictError are available for application code. (The render path
still catches FlowpanelAuthError if you throw one from a page — see below.)
FlowpanelValidationError
export class FlowpanelValidationError { constructor(fieldErrors: Record<string, string>, message?: string); }The one subclass with extra state: fieldErrors, keyed by field name — and it
is the first constructor argument, with the message second:
constructor(fieldErrors: Record<string, string>, message = "Validation failed");Forms render the entries against the matching inputs instead of showing one banner.
import { FlowpanelValidationError } from "@flowpanel/kit";
throw new FlowpanelValidationError({
email: "That address is already registered",
});
// with a banner message as well:
throw new FlowpanelValidationError({ email: "Already registered" }, "Could not save");Config-time errors are not in this hierarchy
defineAdmin throws plain Errors, deliberately: they happen while your
config module evaluates, long before there is a request to map a status onto.
They carry no code, no safeMessage and no status, and no hooks.onError
sees them — the process fails to boot instead.
What they cover: duplicate resource names, dashboard paths and queue keys;
duplicate action keys within one list; a resource or queue named after a
reserved route segment (dashboards, drawer, queues); a column a resource
points at that the adapter does not report; an omitted columns on a ref the
adapter cannot introspect; an unresolvable cross-resource reference; and a
rowClick: "drawer" with no drawer.
Each names the config path at fault and, where it can, the near match. See Troubleshooting for the full table.
On the page render path
FlowpanelAuthError and FlowpanelAccessError thrown while rendering a page are
caught by the shell, not surfaced as a crash:
auth.signInUrlis set → redirect there.auth.forbiddenUrlis set and the visitor is signed in → redirect there.- Neither → an inline "Sign in required" or "Access denied" panel.
Other errors propagate to your Next.js error boundary.
Returning versus throwing
Inside an action's run you have both options:
run: async (row, input, ctx) => {
if (row.status === "refunded") {
return { ok: false, error: "Already refunded" }; // 200 with a failure body
}
throw new FlowpanelConflictError("Order is locked"); // 409
}Return { ok: false } for an outcome the operator caused and can fix — the UI
shows the message next to the action. Throw when the request itself was not
valid, and you want the status code to say so.
Errors the routes produce themselves
Some generated-UI responses never pass through the hierarchy at all — the
route answers directly with a string error. These compatibility routes are
distinct from the v1 JSON CRUD API and protected controllers, whose
FlowpanelResultError contains code, message, optional fieldErrors, and
requestId.
400 — malformed request body
A POST that declares Content-Type: application/json but carries a body that
does not parse is rejected before anything else runs:
{ "ok": false, "error": "invalid JSON body" }That covers the row-, bulk-, dashboard- and drawer-action routes and the inline-cell update. A non-JSON content type is parsed as form data instead and never reaches this branch.
404 — unknown resource, action or dashboard
The message depends on NODE_ENV, on purpose. In production it is terse,
so the response confirms nothing about what does exist:
{ "ok": false, "error": "resource not found" }Outside production it names what was requested and what is registered, which turns "why is my action 404-ing" into a one-look answer:
{
"ok": false,
"error": "action not found: \"refnud\". Registered actions: \"refund\", \"cancel\"."
}kind is one of resource, action or dashboard.
403 / 422 from the guard pipeline
403 Cross-origin write requests are not allowed. when the default browser
origin guard blocks a mutation; 403 This admin is read-only. when
readOnly: true blocks a write;
422 too many ids (max 1000) when a bulk action exceeds its cap;
422 validation failed with an issues array when an action form fails
validation. See Actions → Validation.