philiprehberger-try
Concise error handling with fallbacks, chained recovery, and timeout wrapping
Requirements
- Ruby >= 3.1
Installation
Add to your Gemfile:
gem "philiprehberger-try"
Or install directly:
gem install philiprehberger-try
Usage
require "philiprehberger/try"
Basic wrapping
Wrap any risky expression with Try.call. It returns a Success or Failure:
result = Philiprehberger::Try.call { Integer("42") }
result.success? # => true
result.value # => 42
result = Philiprehberger::Try.call { Integer("nope") }
result.failure? # => true
result.error # => #<ArgumentError: invalid value for Integer(): "nope">
Default values with or_else
Provide a fallback value when the operation fails:
value = Philiprehberger::Try.call { Integer("nope") }
.or_else(0)
.value
value # => 0
Chained recovery with or_try
Chain multiple recovery strategies. The first success wins:
result = Philiprehberger::Try
.call { fetch_from_cache(key) }
.or_try { fetch_from_database(key) }
.or_try { fetch_from_api(key) }
result.value! # returns the first successful result or raises
Handling specific exceptions with on
Recover from specific exception types:
result = Philiprehberger::Try.call { parse_config(path) }
.on(Errno::ENOENT) { |_e| default_config }
.on(JSON::ParserError) { |e| raise ConfigError, "Invalid JSON: #{e.}" }
Side effects with on_error
Log or report errors without changing the result:
result = Philiprehberger::Try.call { risky_operation }
.on_error { |e| logger.warn("Operation failed: #{e.}") }
.or_else(fallback)
Transforming values with map
Transform a successful value. Failures propagate unchanged:
result = Philiprehberger::Try.call { File.read("data.json") }
.map { |contents| JSON.parse(contents) }
.map { |data| data.fetch("key") }
result.value # => parsed value or nil if any step failed
Chaining with flat_map
Chain operations that return Try results without double-wrapping:
result = Philiprehberger::Try.call { "42" }
.flat_map { |v| Philiprehberger::Try.call { Integer(v) } }
.flat_map { |v| Philiprehberger::Try.call { v * 2 } }
result.value # => 84
Recovering from errors
Transform a failure into a success based on the error:
result = Philiprehberger::Try.call { raise ArgumentError, "bad input" }
.recover { |e| "default value" }
result.value # => "default value"
Mapping Errors
Transform a Failure's error into a different exception without recovering:
require 'philiprehberger/try'
result = Philiprehberger::Try.call { File.read('missing.txt') }
.map_error { |e| MyError.new("read failed: #{e.}") }
Success#map_error is a no-op. On Failure, the block return is wrapped in a new Failure. If
the block returns a non-Exception value, it is wrapped in RuntimeError so that Failure#error
always exposes an Exception.
Side effects with tap
Execute side effects without changing the result:
result = Philiprehberger::Try.call { 42 }
.tap { |r| puts "Got: #{r.value}" }
result.value # => 42
Filtering values
Convert a Success to Failure when the value does not satisfy a predicate:
result = Philiprehberger::Try.call { parse_age(input) }
.filter { |v| v >= 0 }
.or_else(0)
result.value # => parsed age, or 0 if negative or parse failed
A failed filter produces a Failure wrapping ArgumentError("filter condition not met").
Pattern matching
Success and Failure support Ruby 3.x case/in pattern matching:
case Philiprehberger::Try.call { Integer(input) }
in { success: true, value: Integer => v }
puts "Parsed: #{v}"
in { success: false, error: ArgumentError => e }
puts "Bad input: #{e.}"
end
Combining results with all
Combine multiple Try results into one. Returns Success with an array of all values, or the first Failure:
a = Philiprehberger::Try.call { Integer("42") }
b = Philiprehberger::Try.call { Integer("7") }
result = Philiprehberger::Try.all(a, b)
result.value # => [42, 7]
Also accepts lambdas, evaluated lazily with short-circuit on first failure:
result = Philiprehberger::Try.all(
-> { Integer("42") },
-> { Integer("nope") },
-> { Integer("99") } # never called
)
result.failure? # => true
result.error # => #<ArgumentError: invalid value for Integer(): "nope">
Timeout support
Add a timeout constraint to any operation:
result = Philiprehberger::Try.call(timeout: 5) { slow_http_request }
result.failure? # => true if it took longer than 5 seconds
result.error # => #<Timeout::Error: execution expired>
API
| Method | On Success | On Failure |
|---|---|---|
Try.call(timeout: nil) { block } |
Returns Success wrapping block result |
Returns Failure wrapping exception |
Try.all(*items) |
Success with array of values if all succeed |
First Failure encountered |
#value |
Returns the wrapped value | Returns nil |
#value! |
Returns the wrapped value | Raises the stored exception |
#success? |
true |
false |
#failure? |
false |
true |
#error |
N/A | Returns the stored exception |
#or_else(default) |
Returns self | Returns Success.new(default) |
#or_try { block } |
Returns self | Calls Try.call with the block |
#on(ExceptionClass) { block } |
Returns self | If error matches, returns Try.call { block } |
#on_error { block } |
Returns self | Calls block for side effect, returns self |
#map { block } |
Wraps block result in new Try.call |
Returns self |
#flat_map { block } |
Chains block returning Try | Returns self |
#recover { block } |
Returns self | Wraps block result in Try.call |
#map_error { block } |
Returns self | Wraps block result (or RuntimeError) in new Failure |
#filter { block } |
Returns self if truthy, Failure if falsy |
Returns self |
#deconstruct_keys(keys) |
{ success: true, value: } |
{ success: false, error: } |
#tap { block } |
Calls block, returns self | Calls block, returns self |
#transform(on_success:, on_failure:) |
Applies on_success lambda |
Applies on_failure lambda |
Development
bundle install
bundle exec rspec
bundle exec rubocop
Support
If you find this project useful: