philiprehberger-try

Tests Gem Version Last updated

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.message}" }

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.message}") }
  .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.message}") }

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.message}"
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:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT