Result Objects - Errors Without Exceptions

??? words · ??? min read

In Ruby, errors and failures are typically implemented with exceptions. In some situations, however, exceptions may not be the best choice. This article covers some of the problems with exceptions, and introduces a functional, alternative approach to error handling.

Sometimes Exceptions Suck

Pop quiz: assuming there are no bugs, what exceptions could be raised by the following line of code?

user = register_new_user(params)

It’s hard to know, without looking at the implementation:

def register_new_user(params)
  new_user = User.new(params)
  authorize! :create, new_user

  new_user.save!

  send_welcome_email(new_user)
end

Even after reading the implementation, it’s still difficult to know for sure. Some of the possible exceptions would be obvious to a Rails developer, and you can probably guess which methods could throw other exceptions, but exceptions could be raised by any method call, any ActiveRecord callback, any third-party gem being used – the possibilities are endless.

Which exceptions are expected error cases, and which ones are unexpected bugs? Which ones should be rescued, and which ones shouldn’t? The answer is not clear.

Exceptions are completely implicit, and that makes them hard to predict. If you can’t predict all the expected error cases, then error cases will not be handled well. Unpredictable code leads to bugs.

If you’re interested to see a good use for exceptions, see Raise On Developer Mistake.

John Nunemaker recently published Resilience in Ruby: Handling Failure, which has good demonstrations of these issues.

This is why developers hate writing exception handling code, in basically every programming language. Ask Java developers how they feel about checked exceptions. Anders Hejlsberg, the lead architect of C#, has this to say:

In a lot of cases, people don’t care. They’re not going to handle any of these exceptions. There’s a bottom level exception handler [that] is just going to bring up a dialog that says what went wrong […] but they’re not actually interested in handling the exceptions.

Introducing Result Objects

Now I want to introduce a different way of handling errors using result objects from the Resonad gem. The key differences are:

  • Errors are part of the return value, not an exception.

  • It separates expected error cases from unexpected bugs. Expected errors are available through result.error, and unexpected bugs are exceptions.

  • All expected error cases are automatically “caught,” without having to guess what they are.

  • The design makes it difficult to “forget” to handle error cases.

The calling method would look something like this:

result = register_new_user(params)
if result.success?
  handle_success(result.value)
else
  handle_failure(result.error)
end

And the implementation would look something like this:

def register_new_user(params)
  authorize(:create, User.new(params))
    .and_then { |user| save_model(user) }
    .on_success { |user| send_welcome_email(user) }
end

def authorize(permission, model)
  authorize! permission, model
  Resonad.Success(model)
rescue AuthorizationFailed => error
  Resonad.Failure(error)
end

def save_model(model)
  if model.save
    Resonad.Success(model)
  else
    Resonad.Failure(model.errors)
  end
end

def send_welcome_email(user)
  UserMailer.welcome(user).deliver_now
end

These Resonad result objects are wrappers for either a successful value, or an error. If result.successful? returns true, then result.value is the successful value. Otherwise result.error will contain some kind of error description.

The on_success method is used for causing side effects without affecting the result. In the code above, it indicates that send_welcome_email is expected to always succeed. If it does fail, by raising an exception, that is an unexpected bug.

The authorize and save_model methods have expected error cases. They return either Resonad.Success or Resonad.Failure. It is not a bug when these methods return Resonad.Failure. The app should handle these kinds of failures, and recover/respond appropriately.

Any methods that return result objects can be chained together with and_then. The and_then method will only run its block on success, and skip the block on failure. Failures at any stage will be passed down the chain, untouched.

As an example, since register_new_user also returns a Resonad, it could be chained even further:

check_registrations_are_open
  .and_then { register_new_user(params) }
  .and_then { |user| create_placeholder_data_for(user) }

Appropriate Uses

There is no need to use a result object:

  • When the method should always succeed. Instead, just throw an exception to indicate that there is a bug.

  • When there is only a single failure case. You can just return nil, false, or some other sentinel value to indicate failure.

  • When the error is locally recoverable. If you can recover from the error, the method doesn’t need to fail. Sometimes it’s appropriate to just return a null object, an empty array, or maybe some dummy data.

Result objects are appropriate:

  • When an operation can fail in multiple different ways.

  • When operations need to be chained together, and each individual operation can fail.

  • When it’s important for callers to know that the method could possibly fail, because they strongly encourage the developer to check for error cases.

In a web app, result objects work well with service/interactor/command objects. The business logic in this area often includes many different failure cases that need to be handled. It’s also common for units of business logic to be chained together.

Available Implementations

Result objects are not a new idea. You can find implementations in multiple languages by searching for “result monad”. In fact, “Resonad” is a combination of “result” and “monad”.

Here is a list of Ruby implementations:

  • Resonad
    This is my newly-released gem, and the one used in the code examples of this article.

  • dry-monads
    This gem contains a collection of different monads. The Either monad can be used as a result object, where Right is success and Left is failure. Also take a look at the Try monad, which is similar, but captures exceptions. These monads can by chained together with dry-transaction.

  • GitHub::Result
    This is the implementation used in John Nunemaker’s article, and is part of the github-ds gem.

  • monadic
    Has an Either monad, inspired by Scala.

  • result-monad
    I just found this recently, but it looks very similar to Resonad.

  • Write your own.
    Honestly, it’s not much work to write a result object class. A custom implementation might fit your project better, and give you one less gem dependency.

Got questions? Comments? Milk?

Shoot an email to [email protected] or hit me up on Twitter (@tom_dalling).

← Previously: Methods Can Be Longer Than Five Lines

Next up: Refactoring From Inheritance To Composition To Data →

Join The Pigeonhole

Don't miss the next post! Subscribe to Ruby Pigeon mailing list and get the next post sent straight to your inbox.