Docs/Routing & Data/Error Handling

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:

TypeScript
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:

JSON
{
  "error": {
    "status": 422,
    "code": "invalid_input",
    "message": "Input is invalid",
    "details": {
      "email": [
        "Email is required"
      ]
    }
  }
}

details is optional.

HTTP error responses default to:

http
Cache-Control: no-store
Content-Type: application/json

The 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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
import {
    throwHttpError,
} from "bcp/error";

throwHttpError(
    403,
    {
        code:
            "billing_access_denied",
        message:
            "Billing access denied",
    }
);

HttpError contains:

TypeScript
{
    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:

TypeScript
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:

JSON
{
  "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:

TypeScript
import {
    tooManyRequests,
} from "bcp/error";

return tooManyRequests(
    "Try again later",
    {
        retryAfter:
            60,
    }
);

Response:

http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Cache-Control: no-store

Generic status responses

Use errorResponse() when no convenience helper exists:

TypeScript
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:

text
badRequest()            400
unauthorized()          401
forbidden()             403
notFoundResponse()      404
conflict()              409
unprocessableEntity()   422
tooManyRequests()       429
internalServerError()   500
serviceUnavailable()    503

Use errorResponse(status, options) for other 4xx and 5xx statuses.

Validation integration

A practical API pattern combines bcp/validation with bcp/error:

TypeScript
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:

text
notFound()             -> page 404 UI convention
bcp/validation         -> typed input validation
bcp/error              -> HTTP/application error responses

Unexpected 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.