Rustic Nil Handling in Ruby

Ad-hoc nil usage causes a lot of problems in Ruby. In this post we explore a more explicit way to handle nil conditions, inspired by Rust.

For the past six months or so I've been working an NES emulator in Rust. As you might expect, I've learned a lot about rust, and even more about NES internals. But the experience has also changed the way I look at Ruby.

Specifically, it's made me more than a little paranoid about methods that return nil.

If You Don't Stand for Something, You'll Fall for Anything

What does nil mean in Ruby? Almost anything. When a method returns nil, it could mean:

  • The method has no return value
  • There's usually a return value but not this time
  • It returns a value from the database, which is NULL
  • Something unexpected happened

This makes code hard to read and is the main cause of the most common Ruby exception in Ruby: NoMethodError. As part-owner of an exception monitoring service, NoMethodError is putting my kid through school.

Look at the following code. It returns nil most of the time because if statements evaluate to nil when the conditional doesn't match and there's no else.

def color_name(rgb)
  if rgb == 0x000000
    "black"
  end
end

color_name("#FFFFFF").titleize
=> NoMethodError: undefined method `titleize' for nil:NilClass

If you're an experienced Ruby developer, you know this rabbit hole goes much deeper. Sometimes these different meanings of nil overlap in strange ways, making it impossible to know whether — for example — a value in a database is NULL, or there's no value in the database at all.

A Better Way

In Rust there's no such thing as nil. Instead, when we want to signify that a function sometimes returns a value and sometimes returns "nothing" we use an Option.

Options are a type that either contains some specific value, or contains no value. Here's what they look like in code:

Option::Some(42); // Wraps the number 42 in an option
Option::None;     // Indicates "no result"

This is already looking better than our ad-hoc nil usage, but it gets even better. The Rust compiler forces you to consider the None case. You can't accidentally ignore it.

match my_option {
  Some(x) => do_something_with_x(x),
  // If you remove the `None` match below, this code
  // won't compile.
  None => do_the_default_thing()  
}

So we could write our color naming example in rust like so:

fn color_name(rgb: u32) -> Option<String> {
    if rgb == 0x000000 {
      Some("black".to_owned())
    } else {
      None
    }
}

Now we're forced to handle both the Some and None conditions:

let name = color_name(0xFFFFFF);

let name = match color_name(0xFFFFFF) {
  Some(value) => value,
  None => "unknown".to_owned(),
}

Sure this is a little verbose and weird looking, but it makes it impossible to ignore the case when a function doesn't return a useful value. That means code that's easier to understand and maintain.

Implementing Option in Ruby

What Ruby lacks in rigor it makes up for in flexibility. I thought it'd be interesting to try to implement something like Option in Ruby.

We can't create compile-time errors in Ruby, since it's an interpreted language. But we can cause incorrect code to always raise an exception, instead of only raising an exception when you hit an edge case.

First, let's create two classes. Some holds a read-only value. None is empty. They're as simple as they seem.

  class Some
    attr_reader :value
    def initialize(value)
      @value = value
    end
  end

  class None
  end

Next, we'll create our Option class which holds either Some or None and only lets us access them when we provide handlers for both.

class Option
  def initialize(value)
    @value = value
  end

  def self.some(value)
    self.new(Some.new(value))
  end

  def self.none()
    self.new(None.new)
  end

  def match(some_lambda, none_lambda)
    if @value.is_a?(Some)
      some_lambda.call(@value.value)
    elsif @value.is_a?(None)
      none_lambda.call()
    else
      raise "Option value must be either Some or None"
    end
 end
end

Finally, we can rewrite our color example to use the new Option class:

def color_name(rgb)
  if rgb == 0x000000
    Option.some("black")
  else
    Option.none()
  end
end

puts color_name(0x000000).match(
  -> value { value },
  -> { "no match" })

# Prints "black"

Conclusion

I've yet to try this technique in a real project. I think it could definitely prevent a lot of the NoMethodErrors that always slip into production. It is a bit cumbersome looking, and not very Rubyish but I imagine that with some refinement a more pleasant syntax would emerge.

What to do next:
  1. Try Honeybadger for FREE
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Start free trial
    Easy 5-minute setup — No credit card required
  2. Get the Honeybadger newsletter
    Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo

    Starr Horne

    Starr Horne is a Rubyist and Chief JavaScripter at Honeybadger.io. When she's not neck-deep in other people's bugs, she enjoys making furniture with traditional hand-tools, reading history and brewing beer in her garage in Seattle.

    More articles by Starr Horne
    Stop wasting time manually checking logs for errors!

    Try the only application health monitoring tool that allows you to track application errors, uptime, and cron jobs in one simple platform.

    • Know when critical errors occur, and which customers are affected.
    • Respond instantly when your systems go down.
    • Improve the health of your systems over time.
    • Fix problems before your customers can report them!

    As developers ourselves, we hated wasting time tracking down errors—so we built the system we always wanted.

    Honeybadger tracks everything you need and nothing you don't, creating one simple solution to keep your application running and error free so you can do what you do best—release new code. Try it free and see for yourself.

    Start free trial
    Simple 5-minute setup — No credit card required

    Learn more

    "We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release."
    — Michael Smith, Cofounder & CTO of YvesBlue

    Honeybadger is trusted by top companies like:

    “Everyone is in love with Honeybadger ... the UI is spot on.”
    Molly Struve, Sr. Site Reliability Engineer, Netflix
    Start free trial