Imported from andymai/brepjs (
.claude/skills/result-error-handling/SKILL.md). Install upstream withnpx skills add andymai/brepjs --skill result-error-handling. Copyright stays with the author.
Result types and error handling
Every fallible operation in brepjs returns Result<T, BrepError> instead of throwing. The type, all combinators, and the extraction helpers live in src/core/result.ts (zero internal imports — a pure foundation module). Error kinds, the code catalog, and per-kind constructors live in src/core/errors.ts.
The two failure channels
Pick the channel before writing any error-handling code:
| Situation | Channel | Mechanism |
|---|---|---|
| Expected failure (bad input, kernel op failed, unsupported capability, file won't parse) | Result |
err(validationError(...)), kernelCall(...), etc. |
| Programmer bug / broken invariant ("this can never happen") | Throw | bug(location, message) from src/utils/bug.ts — throws BrepBugError, never meant to be caught |
Out-of-bounds index that is valid by construction (under noUncheckedIndexedAccess) |
Throw | safeIndex(arr, i, context) in src/core/errors.ts — the sanctioned arr[i]! replacement |
| Cooperative cancellation | Throw | if (signal?.aborted) throw signal.reason (see src/topology/booleanFns.ts) |
The rule for Layers 2–3 (.claude/commands/new-operation.md, CLAUDE.md): never throw for expected failures — return ok(...) or err(...). No ESLint rule enforces this mechanically, so it must be applied by discipline in every new *Fns.ts function. The exceptions above (bug(), safeIndex(), abort rethrows) are the only tolerated throws.
The one sanctioned Result→throw boundary is the fluent shape() facade in src/topology/wrapperFns.ts: every chainable method funnels through an internal unwrapOrThrow that throws BrepWrapperError on Err. See "The throwing boundary" below.
Constructing errors
Prefer kernelCall for kernel operations
src/core/kernelCall.ts is the standard error-construction path in *Fns.ts files. It wraps try/catch, casts the result, translates cryptic OCCT messages, and auto-attaches a suggestion:
// src/topology/shapeFns.ts
return kernelCall(
() => getKernel().downcast(shape.wrapped),
BrepErrorCode.CLONE_FAILED,
'Failed to clone shape'
) as Result<T>;
Three variants:
| Helper | Returns | Use when |
|---|---|---|
kernelCall(fn, code, message, kind?) |
Result<AnyShape> |
Kernel call returns a KernelShape; auto-runs castShape() |
kernelCallRaw<T>(fn, code, message, kind?) |
Result<T> |
Kernel call returns anything else (string, number, array) |
kernelCallScoped(fn, code, message, kind?) |
Result<AnyShape> |
fn(scope) needs intermediate kernel allocations — the DisposalScope is disposed even on the error path (see the memory-and-disposal skill) |
On exception, the error message becomes `${message}: ${translated}` where translateKernelError() (src/core/kernelErrorTranslation.ts) maps ~12 cryptic OCCT patterns (BRepAlgoAPI failures, fillet radius too large, degenerate geometry, ...) into actionable text with the original appended as (kernel: ...). Translation applies only when kind is 'KERNEL_OPERATION' (the default). ERROR_CODE_SUGGESTIONS in the same file maps a dozen codes (FUSE_FAILED, CUT_FAILED, *_NOT_3D, SWEEP_FAILED, LOFT_FAILED, DRAFT_FAILED, ...) to suggestion strings that ride along automatically.
Manual construction: pick the kind-matching constructor
BrepError is a plain object: { kind, code, message, suggestion?, cause?, metadata? } (src/core/errors.ts). There are 9 kinds, each with a constructor sharing the signature (code, message, cause?, metadata?, suggestion?):
| Kind | Constructor | Use for |
|---|---|---|
VALIDATION |
validationError |
Bad input parameters (check these first, before touching the kernel) |
KERNEL_OPERATION |
kernelError |
Kernel op failed (prefer kernelCall unless building the error by hand) |
TYPE_CAST |
typeCastError |
Result was not the expected shape type (e.g. boolean returned non-3D) |
COMPUTATION |
computationError |
Geometric computation failed (intersection, skeleton, center of mass) |
IO |
ioError |
Import/export failure |
QUERY |
queryError |
Shape query failure (e.g. finder not unique) |
MODULE_INIT |
moduleInitError |
Initialisation failure |
UNSUPPORTED |
unsupportedError |
Capability not supported by the current kernel (ADR-0006) — see the kernel-abstraction skill |
SKETCHER_STATE |
sketcherStateError |
Currently unused in src; exists for sketcher state transitions |
Always thread context through:
cause: the original exception. Dropping it destroys kernel diagnostics.metadata: structured context. Real example fromsrc/topology/modifierFns.ts:
return err(
kernelError('FILLET_FAILED', `Fillet operation failed: ${raw}`, e, {
operation: 'fillet',
edgeCount: selectedCount,
radius,
})
);
suggestion: a recovery hint. Theshape()wrapper folds it into the thrown message ("...\nSuggestion: ..."), so it reaches users.
Error codes
BrepErrorCode (src/core/errors.ts) is an as const catalog of ~124 codes grouped by category, with a matching literal-union type. Use BrepErrorCode.X instead of a raw string whenever the code exists — but know two caveats:
BrepError.codeis typedstring, not the union — a raw-string typo compiles fine. The catalog is advisory; checking it is on the author.- The catalog is incomplete: dozens of codes exist in src only as raw string literals (
FILLET_FAILED,WIRE_NOT_CLOSED,THREAD_INVALID_PITCH, and whole families). Grep before assuming a code is new.
To add a new code:
- Add the constant to
BrepErrorCodeinsrc/core/errors.tsunder its category group (kernel-op, validation, IO, ...). - Use it via the kind-matching constructor, or pass it to
kernelCall. - Optionally add an entry to
ERROR_CODE_SUGGESTIONSinsrc/core/kernelErrorTranslation.tssokernelCallauto-attaches a recovery hint. - Optionally add a row to the tables in
docs/errors.md(hand-maintained, no CI check — see the staleness warning below).
Consuming Results
| Need | Use | Notes |
|---|---|---|
| Branch on outcome | isOk(r) / isErr(r) |
Type guards; narrow to Ok<T> / Err<E> |
| Handle both arms as an expression | match(r, { ok: v => ..., err: e => ... }) |
|
| Extract in a test or script | unwrap(r) |
Throws with [kind] CODE: message formatting on Err |
| Extract with fallback | unwrapOr(r, default) / unwrapOrElse(r, fn) |
Never use to paper over failures the caller should see |
| Chain fallible steps | andThen (alias flatMap), or pipeline(input).then(fn).then(fn).result |
Both short-circuit on first Err |
| Transform value / error | map / mapErr / mapBoth |
|
| Combine many | collect(results) (alias all), zip(a, b) |
collect short-circuits on first Err; zip is re-exported from src/index.ts as zipResults |
| Side-effect without consuming | tap / tapErr |
tapErr is the idiomatic "log and pass through" |
| Wrap throwing code | tryCatch(fn, mapError) / tryCatchAsync |
Use at boundaries with throwing third-party code |
| Void success | OK constant (Ok<Unit>) |
For operations with nothing to return |
| Nullable → Result | fromNullable(value, errorFn) |
The unwrap() policy (CLAUDE.md, docs/getting-started.md): sanctioned in tests (the standard extractor), scripts/examples, and internal calls that are infallible by construction — e.g. unwrap(resolvePlane('XY', origin)) in src/core/planeOps.ts, where the input is a known-valid literal. Never use it on a user-facing fallible path in production code; use isOk()/match() there.
Note the kernel-free subpath brepjs/core (src/core.ts) exports only a subset of combinators — no pipeline, mapBoth, tap/tapErr, fromNullable, or/orElse, zip. The full set is on the main brepjs entry.
For Results inside disposal scopes, withScopeResult / withScopeResultAsync in src/core/disposal.ts combine DisposalScope cleanup with a Result-returning body (documented in src/core/README.md).
The throwing boundary: shape() and BrepWrapperError
The fluent shape() wrapper (src/topology/wrapperFns.ts) auto-unwraps every Result and throws BrepWrapperError on Err. The class carries code, kind, suggestion?, metadata?, and its message includes the suggestion when present. Gotcha: error.name is set to 'BrepError' even though the class is BrepWrapperError — match with instanceof BrepWrapperError (exported from src/index.ts), not by name. The catch pattern is shown in docs/cheat-sheet.md.
Escape hatches on the wrapper: .applyResult(fn) unwraps a user-supplied Result-returning function; .done() / .val exit back to plain handles. docs/which-api.md frames the fluent-vs-functional trade-off around exactly this Result-handling difference.
Silent failures: symptom → cause → fix
| Symptom | Likely cause | Fix |
|---|---|---|
| Operation "succeeds" but geometry is missing/wrong downstream | An Err was discarded (result assigned, never checked) |
Check every Result; use tapErr to log, match to handle, or propagate with early return on isErr |
| Failure invisible until far away | unwrapOr(r, fallback) masking a real error |
Reserve unwrapOr for genuinely optional values; otherwise propagate the Err |
Called unwrap() on an Err: [KERNEL_OPERATION] ... |
unwrap() on a fallible path |
Read the formatted [kind] CODE: message; handle with isOk/match at that call site |
| Error message is cryptic OCCT text with no context | Hand-rolled try/catch instead of kernelCall, or cause dropped |
Route kernel calls through kernelCall/kernelCallRaw/kernelCallScoped; always pass the caught exception as cause |
Plain Error thrown from a *Fns.ts function |
Layer 2+ rule violated | Convert to err(<kind>Error(code, message, cause)); reserve throws for bug() |
| Typo'd error code compiles and ships | code is typed string |
Use BrepErrorCode.X; add the constant if it does not exist |
| Async code fails against the wrong kernel with confusing errors | withKernel(id, fn) is sync-only — after the first await the callback silently uses the default kernel |
Use getKernel(id) directly in async code (CLAUDE.md gotcha) |
Bad geometry with an Ok result |
Not an error-channel problem — the kernel produced a valid-but-wrong shape | See the debugging-geometry skill |
Additional resources
src/core/result.ts— the full Result API; short and readable, treat it as the reference.src/core/errors.ts— kinds, catalog, constructors,safeIndex; ground truth for codes.src/core/kernelCall.tsandsrc/core/kernelErrorTranslation.ts— the standard construction path and translation/suggestion tables.docs/errors.md— user-facing per-code reference with recovery advice. Partially stale: its kind list omitsUNSUPPORTED, and its code tables drift fromerrors.tsin both directions. When they disagree,src/core/errors.tswins.- The
adding-operationsskill — the full recipe for a new*Fns.tsoperation (validate →err(validationError(...))→kernelCall→ok). - The
writing-testsskill — asserting onErrresults and usingunwrapin tests.