Browse documentation

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); }
PropertyMeaning
codeStable machine-readable tag, e.g. "validation_failed".
safeMessageShown to the operator verbatim. Keep internals out of it.
statusHTTP 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

ClasscodeStatusDefault messageRaised by FlowPanel?
FlowpanelValidationErrorvalidation_failed422Validation failedyes
FlowpanelAuthErrorunauthenticated401Authentication requiredyours to throw
FlowpanelAccessErrorforbidden403Forbiddenyes
FlowpanelFieldAccessErrorfield_forbidden403Field "x" cannot be changed.yes
FlowpanelOperationDisabledErroroperation_disabled403This operation is disabled.yes
FlowpanelUnknownFieldErrorunknown_field400Unknown field: "x".yes
FlowpanelNotFoundErrornot_found404Not foundyes
FlowpanelConflictErrorconflict409Conflictno — yours to throw
FlowpanelRateLimitErrorrate_limited429Rate limit exceededyes

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.signInUrl is set → redirect there.
  • auth.forbiddenUrl is 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.