Result types and error handling
SkillDev toolsThis skill should be used when working with Result<T,E>, BrepError, or error paths in brepjs — deciding whether to throw or return a Result, constructing errors with codes, or handling failures. Trigger phrases include "should this throw or return err", "Called unwrap() on an Err", "add a new error code", "BrepErrorCode", "kernelCall", "unsupportedError", "BrepWrapperError", "the operation failed silently", "error was swallowed", "how do I unwrap this Result", or writing a new *Fns.ts function that can fail.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Result types and error handling skill
What this skill tells your AI
The instructions your AI receives, as published by andymai/brepjs in .claude/skills/result-error-handling/SKILL.md and read by ahel’s review.
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.
Signals
- GitHub stars
- 101
- Forks
- 9
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
result-error-handling- Source
- github.com/andymai/brepjs