A title for your blog

Let’s talk about Results

In my experience, error handling in large Ruby on Rails applications has always been a challenge. If you’ve worked on a substantial Rails codebase, you too have probably thought about how to standardise error handling. How do we consistently return error information when something goes wrong? I’m not talking about exceptions—the “throw your hands in the air and give up” kind of approach. I’m talking about operations that can predictably succeed or fail, where we want to return meaningful information when something doesn’t go as planned.

None of the Rails applications I’ve encountered have truly solved this problem. Error handling is usually a mix of raising exceptions or relying on inconsistent patterns like checking for nil. These approaches often lead to uncaught exceptions, or the dreaded:

undefined method `important_operation' for nil (NoMethodError)

What I want is the expressiveness of exceptions—without the finality associated with them. Something that allows us to indicate success or failure in a standard and ergonomic way across an application.

How do we design a more robust system where success and error states can both be communicated cleanly? At a basic level, Ruby functions can return multiple values, so one option is to return a pair, like this:

def failable_method
  ...
  [success, error]
end

But let’s face it—this approach feels clunky, uninspired, and far from expressive. Worse, it’s just as error-prone as returning nil. It’s unlikely to gain traction in a large application because it doesn’t provide clarity or consistency.

Update 19 Mar 2025

It didn’t occur me at the time but this is similar to the Elixir convention. The pattern Elixir uses for returns is a tuple with success or failure indicated with the first element and the value in the second element.

{:ok, "World"} = File.read("hello.txt")
{:error, :enoent} = File.read("invalid.txt")

Ruby doesn’t have tuples though this could be emulated with a two element Array. But this convention that is deeply ingrained in the Elixir community and it ties in with Elixir’s pattern matching.

What we need is a structured object that clearly expresses the state of a return value. Is it OK, or is it an error? If it’s OK, what’s the value? If it’s an error, what happened?

Result

We can take inspiration from Rust. Any Ruby devs who have also worked with Rust have also probably wished we had something like the Result enum (and the Option enum too but that’s for another time)

This is the definition of the Rust Result

enum Result<T, E> {
   Ok(T),
   Err(E),
}

It has two variants

  1. OK which contains a value of generic type T. This is the non-error value
  2. Err which contains an error of generic type E. This is the error value. to use Result you can do something like the following
// sucess
let answer = Ok(42);
// error
let answer = Err("What is the question?");

Then later you can get the values out again

// success
answer.unwrap(); 
// 42

(You wouldn’t do this to get the results in Rust, unwrapping an Err will crash your program. This is just for illustrative purposes )

Ruby doesn’t have enums like Rust but we can get most of what we want with a simple class.

Result = Data.define(:value, :error_message) do
  def self.ok(value)
    new(value: value, error_message: nil)
  end

  def self.error(message)
    new(value: nil, error_message: message)
  end

  def ok?
    !value.nil?
  end

  def error?
    !error_message.nil?
  end

  def ok
    raise "Cannot get value from error result: #{error_message}" if error?

    value
  end

  def error
    raise "Cannot get error from ok result" if ok?

    error_message
  end

  private

  def initialize(value:, error_message:)
    raise ArgumentError, "Cannot have both value and error" if value && error_message
    raise ArgumentError, "Must provide either value or error" if value.nil? && error_message.nil?

    super
  end
end

We start by defining a Data class. Data was added in Ruby v3.2. It’s like Struct but it gives us an immutable1 value object. Then we have two convenience constructors, ok(value) and error(message). A couple of predicates and finally accessors for the ok value and the error value (Yes there is irony in raising exceptions all over the place.) This can be used like so:

# Success case
result = Result.ok(42)
result.ok?    # => true
result.error? # => false
result.ok    # => 42

# Error case
result = Result.error("What is the question?")
result.ok?    # => false
result.error? # => true
result.error  # => "What is the question?"

Pattern Matching

A lot of the power of the Result enum in Rust comes from its use in pattern matching. Rust pattern matching looks like this.

let file = match File::open(path) {
    Ok(f) => f,
    Err(e) => return Err(anyhow!("Failed to open file {}", e)),
}

File::open(path) returns a Result. The match statement then chooses which branch to take by matching the "type" of the Result. And pattern matching on Result forces you to match both variants. You will get a compiler warning if you don’t check for both Ok and Err

Because we are using the Data class we can use our Result in pattern matching.

  1. Positional Pattern Matching:
case result
in [value, nil]
  # Handle success case with value
in [nil, error]
  # Handle error case with error
end
  1. Keyword Pattern Matching:
case result
in { value: Integer => num, error_message: nil }
  # Handle success case with integer value in num
in { value: nil, error_message: String => msg }
  # Handle error case with msg
end
  1. One-line Pattern Matching:
if result in { value: Integer => num, error_message: nil }
  # Use num here
end

Ruby has had pattern matching since 2.7 and it’s slowly improving, but this code is quite clunky compared to the Rust version. And we’re not really gaining much over the if or switch statements, but we get it for free so ¯\_(ツ)_/¯

Extensions

The Rust Result enum is deeply embedded in the Rust standard library and as such has a lot of associated functions. Some of which would be nice to have in our Ruby implementation.

Return a default value on error.

An ok_or method would be super useful and it’s easy to implement

def ok_or(default_value)
  value || default_value
end

This then gives us a way to provide a fall back for error cases

# Success
result = Result.ok(42)
result.ok_or(0)
=> 42

# Error
result = Result.error("We failed")
result.ok_or(0)
=> 0

Map

Rust Result has a map function for converting one Result into another with different internal types. Map in Ruby is a member of the Enumerable module and is generally used for applying over collections. So in Ruby it’s a bit weird to have a map method on a class that isn’t enumerable. But this allows us to chain operations together that might return a Result. And while we’re getting all functional we can implement the and_then method.

def map
  return self if error?

  Result.ok(yield(ok))
rescue StandardError => e
  Result.error(e.message)
end

def and_then
  return self if error?

  yield(ok)
end

map allows us to convert one Result into another Result and and_then enables us to operate on the ok value, ignoring the error. This allows up to do things like

# Chaining operations
result
  .map { |value| value * 2 }
  .and_then { |value| value + 1 }

This means if we get an ok result we keep processing the value. If we have an error we return the original error. I think this technically makes Result a monad. I’m not certain mainly because I’m not sure what a monad is 😂 dry-rb has a Result in its Monad gem so I guess it is a monad2.

Where

One of the most useful methods on Exception is backtrace. This does what it says on the tin, it returns the back/stack trace from the point where the exception was first raised. I would consider this a nice to have rather than essential for Result. And I’m not bothered about the entire stack trace, just the source location where the Result was created.

def failable_method
  Result.error("we failed")
end

def important_operation
  failable_method
end

result = important_operation
result.error?
=> true
result.where
=> "(irb):7:in `failable_method'"

This mitigates one of the biggest issues with nil returns; where did the nil come from?

The final Result class is here.

result.rb

I have wanted to implement a Result in most projects I have worked on but it is difficult to get a large group of developers to adopt something like this and I’m not the sort of dev who strikes out on their own (most of the time). I’m working on a couple projects at the moment where I’m the lead (and only developer 😂) so I have added this method of handling errors and it’s working really nice. I’m enjoying not having to think about what to return when there is an error and the functional style of operating with return values is nice.


  1. Data has shallow immutability. The Ruby Changes site has an explanation.

  2. dry-rb helpfully explains that a monad is “Simply, a monoid in the category of endofunctors.” Speaking of dry-rb, why not just use that? I’m trying to minimise dependencies and this is a pretty small class. But if I need anything else from dry-rb I’ll reevaluate.