Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Break Versioning](https://www.taoensso.com/break-ve

## [Unreleased]

### Added

- Improve sum type error handling documentation (addresses #439) (@baweaver)

## [1.8.3] - 2025-06-09

Expand Down
11 changes: 11 additions & 0 deletions docsite/source/combining-types/sum.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,14 @@ nil_or_string["hello"] # => "hello"

nil_or_string[123] # raises Dry::Types::ConstraintError
```

## Error Handling

Sum types try each type from left to right. If all types fail, the error from the rightmost type is raised:

``` ruby
Value = FixedAmount | Percentage

# Raises error from Percentage (rightmost), not FixedAmount
Value.call(type: "fixed", value: -1.1)
```
13 changes: 13 additions & 0 deletions lib/dry/types/sum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ module Dry
module Types
# Sum type
#
# Sum types try each constituent type from left to right and return the result
# from the first successful type. If all types fail, the error from the rightmost
# (last attempted) type is raised, not necessarily the most "relevant" error.
#
# @example
# # Given: FixedAmount | Percentage
# # Input: { type: "fixed", value: -1.1 }
# # FixedAmount fails on value constraint, Percentage fails on type mismatch
# # Error raised will be from Percentage (rightmost), not FixedAmount
#
# @api public
class Sum
include Composition
Expand All @@ -19,6 +29,9 @@ def optional? = primitive?(nil)
#
# @return [Object]
#
# @note Tries left type first, then right type. If both fail, raises the
# error from the right (last attempted) type for performance reasons.
#
# @api private
def call_unsafe(input)
left.call_safe(input) { right.call_unsafe(input) }
Expand Down