Rails find_or_initialize_by: How It Works and When to Use It

Learn how Rails find_or_initialize_by works, how it differs from find_or_create_by, and when to use each one with clear, practical examples from real Rails applications.

Rails find_or_initialize_by


If you’ve been writing Rails code for any amount of time, you’ve probably run into situations where you need to either fetch an existing record or build a new one with certain attributes. That’s exactly what Rails find_or_initialize_by is for, and once you understand how it works, you’ll reach for it often. This post covers what it does, how it differs from its close relative find_or_create_by, and how to use it correctly in real application code.


What find_or_initialize_by Does

find_or_initialize_by is an Active Record method that looks for a record matching the given attributes. If it finds one, it returns it. If it doesn’t, it builds a new object with those attributes but does not save it to the database.

That last part is the key detail. The object is initialized but not persisted. You get back an unsaved Active Record object that you can modify and then save yourself.

ruby
user = User.find_or_initialize_by(email: "[email protected]")

If a user with that email exists, user is that existing record. If not, user is a new User instance with email set to "[email protected]", but not yet in the database.

You can check whether the record is new using new_record?:

ruby
user = User.find_or_initialize_by(email: "[email protected]")

if user.new_record?
  puts "This is a new user"
else
  puts "Found existing user: #{user.id}"
end

How It Differs From find_or_create_by

These two methods are easy to confuse because they work the same way up until the moment a matching record isn’t found.

  • find_or_initialize_by: builds a new object in memory, does NOT save it
  • find_or_create_by: builds a new object AND saves it to the database immediately
ruby
# Does NOT hit the database with an INSERT if not found
user = User.find_or_initialize_by(email: "[email protected]")

# DOES hit the database with an INSERT if not found
user = User.find_or_create_by(email: "[email protected]")

Use find_or_initialize_by when you need to set additional attributes before saving, when you want to validate before persisting, or when you’re not sure yet whether you want to save. Use find_or_create_by when you want the record saved immediately with just those attributes.


Setting Additional Attributes Before Saving

The practical advantage of find_or_initialize_by over find_or_create_by is that it gives you control over what happens next. You can assign more attributes to the object before deciding to save it:

ruby
user = User.find_or_initialize_by(email: "[email protected]")
user.name = "Carol"
user.role = "editor"
user.save

If the record already existed, only the name and role assignments change (not the email, since that was the lookup attribute). If it was new, all three get saved.

A cleaner way to write this uses assign_attributes or the block form:

ruby
user = User.find_or_initialize_by(email: "[email protected]") do |u|
  u.name = "Carol"
  u.role = "editor"
end
user.save

The block only runs when the record is new. If the record already exists, the block is skipped entirely. This is useful when you only want to set certain defaults on creation without overwriting existing data.


Using save vs save!

After find_or_initialize_by, you decide how to persist the object. You have two main options:

save: Returns true if the save succeeded, false if validations failed. Use this when you want to handle failure gracefully.

ruby
user = User.find_or_initialize_by(email: "[email protected]")
user.name = "Dave"

if user.save
  puts "Saved successfully"
else
  puts "Errors: #{user.errors.full_messages.join(', ')}"
end

save!: Raises an ActiveRecord::RecordInvalid exception if validations fail. Use this when a failure should be treated as an unexpected error.

ruby
user = User.find_or_initialize_by(email: "[email protected]")
user.name = "Dave"
user.save!  # Raises if invalid

Which one you choose depends on context. In background jobs or data imports where a failure should halt execution, save! makes sense. In controller actions where you want to render an error response, save with a conditional is cleaner.


A Real-World Example: Upserting User Profiles

Here’s a scenario that comes up often. You’re syncing user data from an external API. For each user in the API response, you want to update their profile if they exist, or create it if they don’t:

ruby
def sync_user(api_data)
  user = User.find_or_initialize_by(external_id: api_data[:id])
  
  user.assign_attributes(
    name: api_data[:name],
    email: api_data[:email],
    last_synced_at: Time.current
  )
  
  if user.save
    Rails.logger.info "Synced user #{user.id}"
  else
    Rails.logger.error "Failed to sync user #{api_data[:id]}: #{user.errors.full_messages}"
  end
end

This handles both the create and update case in one clean block. find_or_initialize_by with external_id finds the existing record or builds a new one, assign_attributes sets the latest values regardless, and save persists it.


find_or_initialize_by in Forms and Controllers

Another common use is building form objects in Rails controllers. Say you have a settings page where a user can configure their notification preferences. The settings record might not exist yet for new users:

ruby
def edit
  @settings = NotificationSetting.find_or_initialize_by(user: current_user)
end

def update
  @settings = NotificationSetting.find_or_initialize_by(user: current_user)
  @settings.assign_attributes(settings_params)
  
  if @settings.save
    redirect_to settings_path, notice: "Settings saved"
  else
    render :edit
  end
end

The form at /settings/edit works whether the user has existing settings or not. On submission, find_or_initialize_by either fetches the existing record to update or builds a new one to create. The rest of the action is identical either way.

This pattern avoids a separate find call followed by a conditional build or new. It’s one method, one clear intent.


Scoping find_or_initialize_by With Associations

You can call find_or_initialize_by on an association scope, which scopes the lookup to records belonging to a specific parent:

ruby
tag = post.tags.find_or_initialize_by(name: "ruby")

This looks for a tag named “ruby” that belongs to post. If it exists, you get it back. If not, you get a new Tag object with name: "ruby" and the post_id already set, because the scope carries that context.

This is cleaner than:

ruby
tag = Tag.find_or_initialize_by(name: "ruby", post_id: post.id)

Both work, but the association form is more idiomatic Rails and less error-prone.


Avoiding Common Mistakes

Mistake 1: Forgetting to save

find_or_initialize_by returns an unsaved object when the record is new. If you don’t call save or save!, nothing gets written to the database. This is the most common footgun, especially when migrating code from find_or_create_by.

Mistake 2: Using it for uniqueness enforcement

find_or_initialize_by is not atomic. In a high-concurrency environment, two requests could both call find_or_initialize_by at the same moment, both find no existing record, both build a new object, and both save it. You end up with duplicates.

For uniqueness guarantees, add a database-level unique index and handle the resulting ActiveRecord::RecordNotUnique exception. The method itself does not protect against race conditions.

Mistake 3: Putting logic in the block that should always run

Remember: the block passed to find_or_initialize_by only executes when the record is new. If you put attribute assignments in the block that you want to apply on every call (including updates), move them outside the block.

ruby
# Wrong: role only sets on create, not on update
user = User.find_or_initialize_by(email: params[:email]) do |u|
  u.role = params[:role]
end

# Correct: role updates every time
user = User.find_or_initialize_by(email: params[:email])
user.role = params[:role]
user.save

Understanding patterns like this is part of writing Rails code that behaves predictably under all conditions, which ties directly into reliable software testing approaches where covering both the “found” and “not found” branches is essential. Clean Active Record usage also connects to broader code quality practices covered in discussions of quality assurance in software development.


find_or_initialize_by vs Other Related Methods

Here’s a quick comparison of the full family of related Active Record methods:

Method Finds existing? Saves if new? Returns
find_by Yes N/A Record or nil
find_or_initialize_by Yes No Record or new unsaved object
find_or_create_by Yes Yes Record or new saved object
first_or_initialize Yes (first match) No Record or new unsaved object
first_or_create Yes (first match) Yes Record or new saved object

first_or_initialize and first_or_create work on an existing scope rather than matching specific attributes, so they pair with where clauses:

ruby
user = User.where(role: "admin").first_or_initialize

For most use cases, find_or_initialize_by with named attributes is clearer and more explicit than the where + first_or_initialize pattern. Better readability in Active Record code also feeds into how maintainable a codebase is over time, which is a topic that comes up in software development career conversations fairly often.


Key Takeaways

  • find_or_initialize_by finds a matching record or builds a new unsaved object with the given attributes
  • It does NOT save the record automatically; you must call save or save! yourself
  • The block form runs only when the record is new, making it useful for setting creation-only defaults
  • Use it over find_or_create_by when you need to set additional attributes before saving or when you want control over the save step
  • It is not safe against race conditions in high-concurrency environments; pair it with a database unique index for true uniqueness guarantees
  • Calling it on an association scope (like post.tags.find_or_initialize_by) automatically scopes the lookup and sets the foreign key on new objects

Once this method clicks, you’ll find it removes a surprising amount of conditional logic from controllers and service objects. It’s one of those Rails conveniences that does exactly what it says with no hidden behavior, as long as you remember to save.