This document defines shared error concepts for Prisma Next so we can be consistent across packages and planes (CLI, migration planning, query planning, runtime execution).
- Make error handling composable across modular packages (domain/layer/plane).
- Keep failures actionable (clear “why / fix / docs”) without turning every API into “Result plumbing”.
- Preserve stack traces for bugs and unexpected faults.
- Ensure boundaries are predictable (stable codes, deterministic output, consistent exit codes).
An expected, explainable outcome where a logical condition is not met.
Examples:
- Invalid user input or config shape
- Plan-builder misuse (e.g., missing
from()/ missingselect()) - Capability gating (feature requires capability but it’s absent/false)
- Policy/guardrail blocks (e.g., budgets/lints in strict mode)
Properties:
- Actionable: caller can fix something deterministically.
- Stable: should have a stable code and structured metadata when surfaced.
Recommended handling:
- Internals may throw a structured failure error to abort quickly and keep context/stack.
- System boundaries convert structured failures into a returned
Result/ envelope.
An expected error caused by an external system state, not a bug in Prisma Next.
Examples:
- Database connection refused / timeout
- Network interruptions
- Permission/auth failures (if/when modeled)
- Driver-level errors (e.g., Postgres errors)
Properties:
- Often transient and environment-dependent.
- May be actionable but not always fixable in-process.
Recommended handling:
- Throw through lower layers (drivers/adapters typically throw).
- Catch at boundaries when we can translate to a stable, actionable envelope (while still retaining stack/context where available).
An invariant break or programming error where the system cannot reliably continue.
Examples:
- Unexpected
undefined/ impossible branch - Internal assertion failure
- Serialization/type invariants broken after validation
Recommended handling:
- Throw and fail fast.
- Only catch at the outermost boundary for crash reporting / last-resort formatting, without disguising the issue as an “expected failure”.
We provide a generic Result<T, F> type for representing success or failure outcomes at system boundaries. This type is used when a function can return either a success value or a structured failure.
import type { Result, Ok, NotOk } from '@prisma-next/utils/result';
import { ok, notOk, okVoid } from '@prisma-next/utils/result';
// Success with a value - both T and F must be specified
function divide(a: number, b: number): Result<number, { code: string; message: string }> {
if (b === 0) {
return notOk({ code: 'DIVISION_BY_ZERO', message: 'Cannot divide by zero' });
}
return ok(a / b);
}
// Validation that returns void on success
function validateInput(input: string): Result<void, { code: string; message: string }> {
if (input.length === 0) {
return notOk({ code: 'EMPTY_INPUT', message: 'Input cannot be empty' });
}
return okVoid();
}
// Usage
const result = divide(10, 2);
if (result.ok) {
console.log('Result:', result.value);
} else {
console.log('Error:', result.failure.code);
}Naming and design rationale:
Ok<T>/NotOk<F>mirror theok: true/falsediscriminator propertyNotOkavoids collision with domain-specific "Failure" or "Error" typesvaluefor success,failurefor unsuccessful—distinct names prevent confusionfailure(noterror) distinguishes structured failure data from JSErrorsemantics- No default for
F- Both type parameters are required; the whole point is to strictly type failures, not to propagate JavaScript's untyped error handling
When to use Result:
- At system boundaries (CLI commands, migration runner, SDK entrypoints)
- When a function can fail in expected ways that callers should handle
- When you want to avoid throwing for expected failures
When NOT to use Result:
- Deep within package internals (prefer ergonomic throws + catch at boundary)
- For bugs/unexpected errors (throw and fail fast)
- For streaming APIs (use AsyncIterable that throws on error)
See:
packages/1-framework/0-foundation/utils/src/result.ts
CLI commands use structured errors and convert them to a Result at the command boundary. Non-structured errors propagate (fail fast) to preserve stack traces.
See:
packages/1-framework/1-core/control-plane/src/errors.ts(CliStructuredError)packages/1-framework/3-tooling/cli/src/utils/result.ts(performAction)docs/CLI Style Guide.md("Errors", exit codes)
Plan-building failures are represented as RuntimeError with stable codes like PLAN.INVALID and PLAN.UNSUPPORTED.
Migration runner failures use stable MigrationRunnerErrorCode values (e.g., EXECUTION_FAILED, SCHEMA_VERIFY_FAILED, PRECHECK_FAILED, POSTCHECK_FAILED) returned as part of Result<MigrationRunnerSuccessValue, MigrationRunnerFailure>. This follows the pattern described in "Provide stable codes for 'expected failures'" (see Guidelines section below) where stable codes enable deterministic error handling at system boundaries.
See:
packages/2-sql/3-tooling/family/src/core/migrations/types.ts(MigrationRunnerErrorCode,MigrationRunnerFailure)- SQL lane helpers that throw these for invalid builder usage/capability gating.
Runtime execution is modeled as an AsyncIterable that throws on error. This is a deliberate shape: it allows early abort and preserves context/stack in async workflows.
Guardrails (budgets/lints) may block execution by throwing a structured runtime error (a failure) in strict mode.
See:
packages/1-framework/1-core/framework-components/src/runtime-core.tspackages/2-sql/5-runtime/src/middleware/budgets.tspackages/2-sql/5-runtime/src/middleware/lints.ts
- Within packages, prefer APIs that are ergonomic for the domain (builders, streams, plugin hooks).
- At system boundaries, prefer returning a
Result/envelope for expected failures so callers (CLI/apps/agents) can handle them deterministically.
Boundaries include:
- CLI command actions
- Migration runner entrypoints
- Public SDK entrypoints that are the “edge” of the system
- Catch structured failures that you can translate to an actionable
Result/envelope. - Do not broadly catch and wrap unknown errors; let them fail fast.
- “Failure” surfaces should use stable codes (e.g.,
PN-CLI-4xxx,PLAN.INVALID,BUDGET.*). - Codes are how agents/CI should match and branch—not brittle string matching.
- Bug paths must retain stacks.
- For failures, stacks are still useful for debugging, but user-facing surfaces should emphasize
why/fix/docsand treat stacks as “trace/verbose”.
- If a caller can reasonably continue or present an actionable fix → model as a failure and return/convert to
Resultat the boundary. - If the system is not in a valid state or behavior is undefined → it’s a bug; throw and fail fast.
- If it comes from a driver/network/DB → it’s usually an operational error; throw through, translate at the boundary when helpful.