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.
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.
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?:
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 itfind_or_create_by: builds a new object AND saves it to the database immediately
# 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:
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:
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.
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.
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:
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:
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:
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:
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.
# 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:
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_byfinds a matching record or builds a new unsaved object with the given attributes- It does NOT save the record automatically; you must call
saveorsave!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_bywhen 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.