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
- OK which contains a value of generic type T. This is the non-error value
- 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.
- Positional Pattern Matching:
case result
in [value, nil]
# Handle success case with value
in [nil, error]
# Handle error case with error
end
- 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
- 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.
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.
Data has shallow immutability. The Ruby Changes site has an explanation.↩
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.↩