Avoiding the Silent Fail: Mastering Rails Validations with `find_or_create_by` | Ivan Marynych
← Blog Ruby on Rails

Avoiding the Silent Fail: Mastering Rails Validations with `find_or_create_by`

JUL 09 2024
3 min read
Ruby on Rails / Validations / Database / Error Handling

The Hidden Trap in Ruby on Rails Associations

In the Rails ecosystem, we rely heavily on associations and validations to keep our data clean. However, even seasoned developers can get tripped up by how Rails handles “unsaved” instances.

I recently investigated a bug involving two standard models: City and Country. The logic seemed bulletproof, but the database was throwing a 500 Internal Server Error. Here is how I diagnosed and solved the “Invalid Instance” trap.

The Setup

We have a standard one-to-many relationship:

class Country < ApplicationRecord
  has_many :cities
  validates :name, presence: true # The critical validation
end

class City < ApplicationRecord
  belongs_to :country
end

The “Invisible” Error

Consider this common workflow:

# Attempting to find a country without a name (which violates our validation)
country = Country.find_or_create_by(name: nil)

# Attempting to create a city with that country
city = City.create(name: "San Francisco", country: country)

What happens here? If find_or_create_by fails its validation (because name is nil), it doesn’t return nil. Instead, it returns an unsaved object with an ID of nil.

When we pass that object into City.create, Rails sees a Country object and thinks everything is fine. But when it tries to save the City to the database, it attempts to insert a NULL into the country_id column. If your database has a NOT NULL constraint, the app crashes with a 500 error instead of a graceful validation message.

The Solution: Elegant Presence Checking

To prevent your database from receiving invalid data, you need to ensure you aren’t passing an invalid object into your association. We can leverage the Power of .presence and .id to sanitize the input.

The Refactored Code:

# 1. Get the ID, but use .presence to return nil if the ID is missing
country_id = Country.find_or_create_by(name: nil).id.presence

# 2. Now City.create will trigger a standard Rails validation error
# instead of a database-level crash.
city = City.create(name: "San Francisco", country_id: country_id)

By calling .id.presence, we force the variable to be a true nil if the country wasn’t successfully found or saved. This allows the City model’s belongs_to validation to catch the error properly.

Why This Matters for Your Project

As a developer, I believe that robustness is not accidental. Handling these edge cases ensures:

  • Data Integrity: No “orphan” records in your database.
  • User Experience: Users see helpful validation messages instead of “Something went wrong” pages.
  • Scalability: Cleaner code leads to fewer midnight emergency bug fixes.
Tagged Ruby on RailsValidationsDatabaseError Handling