ToResult

ToResult is a wrapper built over dry-monads to make the Do Notation, Result and Try concepts more handy and consistent to use, in particular to implement the Railway Pattern.

Why I created ToResult

dry-monads is full of edge cases that require to write boilerplate code everytime I want a method to return a Success or Failure, for example:

def my_method
  Success(another_method.call)
rescue StandardError => e
  Failure(e)
end

so I started using Try, that makes the code easier to read and faster to write:

def my_method
  Try do
    another_method.call
  end.to_result
end

but I feel like to_result is not really visible at the end of the code and if you forget to write it (as always happens to me) your application blows up.

But this is not the bigget problem, bear with me.

One of the biggest problem is that we cannot use the Do Notation inside a Try block:

# this will return a Failure(Dry::Monads::Do::Halt)
def my_method
  Try do
    yield Failure('error code')
  end.to_result
end

and you cannot even use yield and rescue in the same method:

# this will return a Failure(Dry::Monads::Do::Halt)
def my_method
  yield Failure('error code')
rescue StandardError => e
  # e is an instance of Dry::Monads::Do::Halt
  Failure(e)
end

because they will raise a Dry::Monads::Do::Halt exception and the original exception will be forever lost if we do not "unbox" the exception with e.result.

Installation

To install with bundler:

bundle add to-result

or with gem:

gem install to-result

Usage

To use it with instances of a class, just include it

require 'to_result'

class MyClass
  include ToResultMixin

  def my_method
    ToResult do
      whatever_method.call
    end
  end
end

or if you want to use it with Singleton Classes:

require 'to_result'

class MyClass
  extend ToResultMixin

  class << self
    def my_method
      ToResult do
        whatever_method.call
      end
    end
  end
end

now you can always use ToResult all the time you wanted to use Success, Failure or Try but with a more convenient interface and consistent behaviour.

Look at this:

ToResult { raise StandardError.new('error code') }
# returns Failure(StandardError('error code'))

ToResult { yield Success('hello!') }
# returns Success('hello!')

ToResult { yield Failure('error code') }
# returns Failure('error code')

ToResult { yield Failure(StandardError.new('error code')) }
# returns Failure(StandardError('error code'))

ToResult([YourCustomError]) { yield Failure(YourCustomError.new('error code')) }
# returns Failure(YourCustomError('error code'))

ToResult([ArgumentError]) { yield Failure(YourCustomError.new('error code')) }
# raises YourCustomError('error code')

Roadmap

I'm already planning to implement some useful features:

  • [x] write more examples/documentation/tests
  • [ ] configurable error logging when an exception is catched inside DoResult e.g. sending the log to Airbrake or whathever service you are using
  • [ ] transform/process the catched error
  • [ ] any other suggestion would be appreciated 😁

Authors