Installs into .claude/skills of the current project.
Are you the author of Functional Handling Errors With Results?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/yutna-functional-handling-errors-with-results)
---
name: functional-handling-errors-with-results
description: Use when a function throws and callers cannot tell, when exceptions cross module boundaries, or when validation must report every problem.
license: MIT
metadata:
pack: functional-design-skills
version: 3.3.0
---
# Handling Errors With Results
**In this pack:** [how much design a task warrants](../functional-design/references/calibration.md),
[the notation](../functional-design/references/notation.md), and
[syntax per language](../functional-design/SKILL.md#language-packs).
## Overview
An error that a caller is expected to handle is part of the function's
contract, so it belongs in the function's type. `Result<T, E>` puts it
there: the function returns either a success or a named failure, and no
caller can forget that failure exists.
Composing such functions is the only mechanic worth learning. A chain of
steps that each may fail forms two tracks: the success track runs through
every step, and the first failure diverts to the failure track and skips
the rest. Once that shape is in place, error handling stops being
scattered defensive code and becomes part of the pipeline's structure.
## When to use
- Designing what a function returns when it can fail
- Exceptions are thrown in one module and caught in another
- A form must report every invalid field, not just the first
- Deciding what an error type should contain
- A pipeline step can fail and the failure must reach the caller
Not for: failures that can be removed by design, which is
[functional-defining-errors-out-of-existence](../functional-defining-errors-out-of-existence/SKILL.md).
## Core rules
1. Rule. **Expected failures are values.** If a caller should act on it, return
it. Reserve exceptions for programmer bugs.
2. Rule. **Name the failures in the domain's words.** `QuoteExpired`, not
`Error500` or a bare string.
3. Default. **One error choice type per workflow.** Steps keep narrow error
types and are lifted at composition, so callers match on one thing.
4. Default. **Short-circuit sequential steps; accumulate independent ones.**
Bind for a pipeline, applicative combination for a form. Accumulating is for
checks that are independent and whose failures the caller can fix in one
pass. Independent steps still stop at the first failure when one of them is
expensive and a cheap one has already failed, or when nothing can be fixed
from a list, as with two reads that failed or a stored row that will not
parse.
5. Rule. **Convert exceptions at the boundary.** Libraries throw; wrap them
once, at the edge, into your own error type. An exception the adapter
cannot classify is returned as an opaque case that callers do not
match on; [error-taxonomy.md](references/error-taxonomy.md) says why.
6. Rule. **Never return a value that stands for a failure the caller must
notice.** No empty string, no `-1`, no null. Those hide the branch the
type should show. A default that the operation's definition makes
ordinary, and that is documented, is not one; see
[functional-defining-errors-out-of-existence](../functional-defining-errors-out-of-existence/SKILL.md).
7. Rule. **Do not log and rethrow.** Return the error; let the edge decide what
to log, once.
## Pattern
Exceptions across boundaries:
```text
priceBooking : Catalogue -> ValidatedBooking -> PricedBooking
-- throws TreatmentNotFound, throws PriceUnavailable
-- caller must read the implementation to know
```
Failures in the type:
```text
type PricingError =
| TreatmentNotFound of TreatmentCode
| PriceUnavailable of TreatmentCode
| TotalTooLarge of Money
priceBooking :
Catalogue -> ValidatedBooking -> Result<PricedBooking, PricingError>
```
Now the failures are visible at every call site, the compiler or the
review process ensures they are handled, and the cases are business
outcomes rather than implementation accidents.
## The two tracks
```text
validate >=> price >=> acknowledge
```
Each step takes a plain value and returns a `Result`. The composition
runs the next step only on success, and passes failures straight
through. That single operator replaces every nested check.
| Operator | Use when |
| ---------- | ------------------------------------------------- |
| `map` | The next step cannot fail |
| `bind` | The next step can fail |
| `mapError` | Converting a step's error to the workflow's error |
| `traverse` | Running a fallible step over a collection |
| `combine` | Two independent results, both failures wanted |
More in [railway.md](references/railway.md).
## Short-circuit or accumulate
The choice depends on what the caller can do with the errors.
**Short-circuit** when the steps are sequential and later steps depend on
earlier ones. Pricing cannot run if validation failed, so reporting both
failures is meaningless.
**Accumulate** when the checks are independent and the caller can fix
them in one pass, whether the caller is a person or a program. A form
with four bad fields should report four problems, not one per
submission.
```text
validatePatient :
UnvalidatedPatient -> Result<Patient, NonEmptyList<ValidationError>>
validatePatient raw =
map3 makePatient
(parseName raw.name |> toValidation)
(parseEmail raw.email |> toValidation)
(parseBirthDate raw.birthDate |> toValidation)
```
See
[applicative-validation.md](references/applicative-validation.md).
## Designing the error type
Ask what the caller will do with it. That determines the shape.
| Caller behaviour | Error shape |
| ------------------------- | ---------------------------------- |
| Branches per failure | Choice type, one case per failure |
| Shows a message to a user | Structured cases, rendered at edge |
| Retries some failures | Cases that distinguish transient |
| Only logs it | One case with context |
| Fixes a form field | Case carrying the field name |
Include the data needed to act or to explain: which treatment code, which
field, which limit. Do not include stack traces, driver codes, or
sentences meant for end users; the edge renders messages, using the
reader's language.
See [error-taxonomy.md](references/error-taxonomy.md).
## Red flags
- A function documented with "throws" that callers must read
- A `catch` block in the middle of business logic
- An error type that is a plain string
- The same exception caught in five places, each handling it differently
- A `Result` whose failure case is ignored at the call site
- Errors from a database driver reaching a controller
- Validation that stops at the first bad field of a form
## Common mistakes
- **Using `Result` for programmer bugs.** A null argument that should be
impossible is a bug; crashing is the honest response.
- **One giant error type for the whole application.** Callers then handle
cases that cannot occur where they are. Scope it to the workflow.
- **Wrapping and rewrapping.** Each layer adding a wrapper produces an
onion no one can match on. Convert once, at the boundary.
- **Losing the cause.** When converting from a library, keep the original
as opaque context for logs, out of the matchable cases.
- **Accumulating when steps depend on each other.** Reporting "price
missing" for a booking that failed validation confuses everyone.
## Related skills
- [functional-defining-errors-out-of-existence](../functional-defining-errors-out-of-existence/SKILL.md)
- [functional-designing-workflow-pipelines](../functional-designing-workflow-pipelines/SKILL.md)
- [functional-composing-functions](../functional-composing-functions/SKILL.md)
- [functional-crossing-io-boundaries](../functional-crossing-io-boundaries/SKILL.md)
## Further reading
- [railway.md](references/railway.md) is the two-track model, its
operators, and how to adapt functions to fit it.
- [applicative-validation.md](references/applicative-validation.md)
covers accumulating errors and when to prefer it.
- [error-taxonomy.md](references/error-taxonomy.md) classifies failures
and says which mechanism each one deserves.