When Systems Can't Agree on the Truth

Engineering Insights

April 29, 2025
No items found.
#
Min Read
When Systems Can't Agree on the Truth

TL;DR

  • Be careful about true vs truthy on the boundaries of your application
  • Consider returning true and false exclusively in predicate methods
  • Consider testing .to eq true instead of .to be_truthy in RSpec

Truthiness in Ruby

Ruby, being duck-typed, does not strictly enforce "boolean" values:

if value
  do_this
else
  do_that
end

In this case, we don't care if value is true or false or 1 or 0 or a complex object. Ruby has a method for determining if any kind of value is "truthy" or "falsey". For Ruby, the rules are fairly lean:

  • nil is falsey
  • false is falsey
  • Everything else is truthy

Therefore:

"Hello World" if 1   # => "Hello World"
"Hello World" if nil # => nil

Now there might be some surprises if you come from other languages:

"Hello World" if "" # => "Hello World"
"Hello World" if 0  # => "Hello World"

Those wouldn't fly in all languages, but that's fine...unless...

Project Boundaries

Let's contrive a thing to make a point:

class ContrivedThing < ApplicationRecord
  def self.foo
    where(id: all.select(&:foo?))
  end

  def foo?
    complicated_foo?
  end

  private

  def complicated_foo?
    # ... do complex work ...
    complicated_derived_thing =~ /fancy regex/
  end
end

And we can test it like:

expect(contrived_thing).to be_foo

But there could be a problem! What if we have lots of ContrivedThings? all.select(&:foo?) loads all the records into memory, does a complicated calculation on each to determine which are foo?-ey, and then wraps them in a huge binds list for query chaining.As much as possible, we'd like to keep this in SQL so we decide to memoize the value of foo? in a boolean column named foo in the database:class ContrivedThing < ApplicationRecord  before_save :memoize_foo  private  def memoize_foo    return unless certain_things_changed? assign_attributes(foo: complicated_foo?)  endendNow we can delete ContrivedThing::foo and ContrivedThing#foo? (because ActiveRecord will create scopes and predicate methods for
boolean columns) and we've vastly improved the performance of the scope.

But there's a catch...

The Truth of Truthiness

We've been using foo? as a truthy/falsey identifier but what is it actually? Well its return value comes from the =~ operator which returns integer or nil.

So we might have:

"string" =~ /string/ # => 0

which is properly truthy in Ruby because there was a match. Unfortunately, we're directly serializing that value to our postgresql database which coerces zero to false.Now we have a bunch of values that used to be truthy in Ruby but now are falsey because postgres converted them to a strict false value.

How Could We Avoid This?

Carefully Considering the Destination

One approach involves converting the value before we head off to the database:

def memoize_foo
  return unless certain_things_changed?

  boolean_value = ActiveModel::Type::Boolean.new.cast(complicated_foo?)
  # Rails 4.2 - ActiveRecord::Type::Boolean.new.type_cast_from_database()
  # Rails 4.1 and below - ActiveRecord::ConnectionAdapters::Column.value_to_boolean
  assign_attributes(foo: boolean_value)
end

Which works wonderfully, but can be forgotten and is only really caught if you happened to write a test like:

thing = ContrivedThing.new()
# setup to allow complicated_foo? to return 0
thing.save
expect(thing).to be_foo

Ensure Boolean Values in Predicate Methods

There's clearly some heat around this topic in general but the official Ruby FAQ recommends:

predicate methods (those whose name ends with a question mark) return true or false.

So one way around our bug is:

def complicated_foo?
  complicated_derived_thing =~ /fancy regex/ ? true : false # => true/false
end

Ensure Boolean Values in Tests

Following the above option, what if our original test wasn't:

expect(contrived_thing).to be_foo

but instead:

expect(contrived_thing.foo?).to eq true

This sort of testing isn't nearly as pretty, but helps prevent these integration pains.

Summary

  • Ruby's handling of predicate methods, predicate matchers, and truthy/falsey
    values allows for light, expressive, pleasing code, but can result in subtle
    bugs when interacting with outside sources.
  • Be extra careful at application boundary points that there are no assumptions
    about how languages interpret different values.
  • When in doubt, be overly specific to ensure values can't be misunderstood.

Learn more about how The Gnar builds Ruby on Rails applications.

Related Insights

See All Articles
Product Insights
Repo Roundup June 16th

Repo Roundup June 16th

Weekly hand-curated list of new, trending and noteworthy projects
Product Insights
Stop Guessing About AI: How We Built a Solution That Actually Answers Your Strategic Questions

Stop Guessing About AI: How We Built a Solution That Actually Answers Your Strategic Questions

Introducing Gnarstradamus: Gnar's AI solution brainstorming tool to get AI use cases specifically for your business
Product Insights
Repo Roundup June 9th

Repo Roundup June 9th

Weekly hand-curated list of new, trending and noteworthy projects
Previous
Next
See All Articles