Routing & Data
Error Handling
Error Handling — Routing & Data documentation for BCP Framework.
Last updated: 2026-08-29T01:11:41
Error Handling
BCP 0.1.20 adds a structured HTTP error API for API routes, form actions, route guards, server data loaders and shared service code.
Import the public helpers from bcp/error:
import {
badRequest,
conflict,
createHttpError,
forbidden,
isHttpError,
notFoundResponse,
throwHttpError,
tooManyRequests,
toErrorResponse,
unauthorized,
unprocessableEntity,
} from "bcp/error";The same helpers are also re-exported by the server-only bcp/server entrypoint.
Standard error envelope
BCP error responses use one JSON shape:
{
"error": {
"status": 422,
"code": "invalid_input",
"message": "Input is invalid",
"details": {
"email": [
"Email is required"
]
}
}
}details is optional.
HTTP error responses default to:
Cache-Control: no-store
Content-Type: application/jsonThe no-store default prevents authentication and validation failures from being cached accidentally. A custom Cache-Control header can override the default when an application intentionally needs another policy.
API routes
Return an error helper exactly like a normal Web Response:
import {
unauthorized,
} from "bcp/error";
export async function GET() {
const user = null;
if (!user) {
return unauthorized();
}
return Response.json({
user,
});
}Custom messages and details are supported:
import {
unprocessableEntity,
} from "bcp/error";
export async function POST() {
return unprocessableEntity(
"Input is invalid",
{
email: [
"Email is required",
],
}
);
}Form actions
BCP actions already accept a Web Response, so error helpers work without a separate transport convention:
import {
conflict,
} from "bcp/error";
export async function createUser(
formData: FormData
) {
const email =
String(
formData.get("email") ?? ""
);
const exists =
email ===
"[email protected]";
if (exists) {
return conflict(
"Email is already in use",
{
field:
"email",
}
);
}
return {
ok: true,
};
}For normal form-field validation, prefer bcp/validation. Use HTTP error responses for request-level failures such as authorization, conflicts, invalid application state or service failures.
Route guards
Route guards may stop navigation with a response:
import {
forbidden,
} from "bcp/error";
export async function guard() {
const role =
"member";
if (
role !== "admin"
) {
return forbidden(
"Admin access required"
);
}
return {
role,
};
}For standard authenticated route protection, requireAuth() and requireRole() from bcp/auth remain the preferred high-level APIs.
Server data loaders
Loaders may return error responses before rendering a page:
import {
serviceUnavailable,
} from "bcp/error";
export async function loader() {
const databaseAvailable =
false;
if (!databaseAvailable) {
return serviceUnavailable();
}
return {
users: [],
};
}notFound() versus notFoundResponse()
BCP has two intentionally different 404 APIs.
Use notFound() from bcp when page rendering should activate the BCP not-found.tsx route UI:
import {
notFound,
} from "bcp";
export default function UserPage() {
const user = null;
if (!user) {
notFound();
}
return <main>User</main>;
}Use notFoundResponse() from bcp/error when the caller needs a JSON HTTP response:
import {
notFoundResponse,
} from "bcp/error";
export function GET() {
return notFoundResponse(
"User not found"
);
}This separation keeps page UI semantics distinct from API response semantics.
Structured HttpError
Shared service code may need to report a typed error without returning a Web Response directly.
Create one with:
import {
createHttpError,
} from "bcp/error";
export async function loadAccount(
id: string
) {
if (!id) {
throw createHttpError(
400,
{
code:
"missing_account_id",
message:
"Account id is required",
}
);
}
}The equivalent shorthand is:
import {
throwHttpError,
} from "bcp/error";
throwHttpError(
403,
{
code:
"billing_access_denied",
message:
"Billing access denied",
}
);HttpError contains:
{
status: number;
code: string;
message: string;
details: unknown;
headers: Headers;
}HTTP error statuses must be integers from 400 through 599.
Converting caught errors
Use isHttpError() when application code needs to branch on the error type:
import {
isHttpError,
toErrorResponse,
} from "bcp/error";
export async function GET() {
try {
await loadAccount("123");
return Response.json({
ok: true,
});
} catch (error) {
if (
isHttpError(error)
) {
console.warn(
error.code,
error.status
);
}
return toErrorResponse(
error
);
}
}Known HttpError values preserve their status, code, message, details and headers.
Unknown exceptions are intentionally converted to:
{
"error": {
"status": 500,
"code": "internal_server_error",
"message": "Internal Server Error"
}
}BCP does not expose the original unknown exception message in that fallback response. Log internal errors on the server instead of returning stack traces, database errors, credentials or other implementation details to clients.
Rate limiting
tooManyRequests() supports Retry-After:
import {
tooManyRequests,
} from "bcp/error";
return tooManyRequests(
"Try again later",
{
retryAfter:
60,
}
);Response:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Cache-Control: no-storeGeneric status responses
Use errorResponse() when no convenience helper exists:
import {
errorResponse,
} from "bcp/error";
return errorResponse(
405,
{
code:
"method_not_allowed",
message:
"Only POST is allowed",
headers: {
Allow:
"POST",
},
}
);Common built-in helpers in 0.1.20 are:
badRequest() 400
unauthorized() 401
forbidden() 403
notFoundResponse() 404
conflict() 409
unprocessableEntity() 422
tooManyRequests() 429
internalServerError() 500
serviceUnavailable() 503Use errorResponse(status, options) for other 4xx and 5xx statuses.
Validation integration
A practical API pattern combines bcp/validation with bcp/error:
import {
unprocessableEntity,
} from "bcp/error";
import {
v,
} from "bcp/validation";
const schema =
v.object({
email:
v.string({
email: true,
}),
});
export async function POST(
request: Request
) {
const result =
schema.safeParse(
await request.json()
);
if (!result.success) {
return unprocessableEntity(
"Validation failed",
{
fieldErrors:
result.fieldErrors,
formErrors:
result.formErrors,
}
);
}
return Response.json(
result.data
);
}Error handling responsibilities
BCP separates three concerns:
notFound() -> page 404 UI convention
bcp/validation -> typed input validation
bcp/error -> HTTP/application error responsesUnexpected framework or programming exceptions remain server errors and should be logged. Do not convert every exception to a successful action data object; use the correct HTTP status when the failure represents an HTTP request outcome.