Note: This is the second post in a series on error handling in Ruby.
As I pursue a coherent approach for error handling in Ruby, I'm bound to come across patterns that I think are smelly. This post attempts to maintain a list of those patterns and explain where I think they fall short.
raise
ing, a Ruby exception (This pattern was initially proposed by Robert Mosolgo in his post Raising Exceptions is Bad.)
One of the earliest patterns I experimented with was using Sorbet union types and custom exception types. Rather than raise
ing an exception as a form of controlling program flow, I simply return it. In this pattern, the return type of error-throwing method (like #sqrt
) will be a union of the result type (Float
) with each of the possible custom exception types.
# typed: strict
require "sorbet-runtime"
class SqrtService
extend T::Sig
class NegativeNumberError < StandardError; end
sig {
params(value: Float)
.returns(T.any(
Float,
NegativeNumberError
))
}
def self.sqrt(value)
if value < 0.0
return NegativeNumberError.new
end
Math.sqrt(value)
end
end
sqrt = SqrtService.sqrt(-5.4)
if sqrt.is_a?(SqrtService::NegativeNumberError)
puts "Received error #{sqrt.class}"
else
puts "doing something else with #{sqrt}"
end
This pattern takes advantage of Sorbet's strong error checking by forcing callers to explicitly consider the error case: without calling is_a?
on the result to narrow the result type, program execution cannot continue down the happy path.
Still, this pattern is confusing for a couple reasons. First, it returns instances of exceptions rather than raising them. This is hardly an idiomatic Ruby pattern: exception types are generally declared in order to raise
and rescue
them. A better alternative, for instance, may be a lightweight result type. In addition, the usage of is_a?
for type narrowing is inexpressive and verbose. For example, I can imagine a program with many nested calls to error-throwing methods, each of which requires another level of is_a?
for the type narrowing. I'm confident that are more composable patterns that allow us to chain operations in a more railway-oriented manner.