Error handling in Ruby: smelly patterns

Note: This is the second post in a series on error handling in Ruby.

  1. Introduction
  2. Smelly patterns

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.

Returning, not raiseing, 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 raiseing 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.