Question
@user.update_languages(
params[:language][:language1],
params[:language][:language2],
params[:language][:language3]
)
lang_errors = @user.errors
logger.debug "LANG_ERRORS 101: #{lang_errors.full_messages.inspect}"
if params[:user]
@user.state = params[:user][:state]
success = success & @user.save
end
logger.debug "LANG_ERRORS 102: #{lang_errors.full_messages.inspect}"
if lang_errors.full_messages.empty?
# ...
end
The update_languages method adds validation errors to @user.errors. However, after calling @user.save, the errors that were initially visible through lang_errors are no longer present. Is Ruby pass-by-reference or pass-by-value, and why does assigning @user.errors to lang_errors not preserve the original errors? How can the error messages be retained without being changed by a later save?
Short Answer
Ruby variables hold references to objects. In this example, lang_errors = @user.errors makes both expressions refer to the same mutable Rails errors object; it does not create a saved copy. A later save can clear and rebuild that object's validation errors. To preserve what was shown at a specific time, copy a snapshot such as @user.errors.full_messages.dup before calling save.
Concept
Ruby is most accurately described as using object references (also called object sharing or pass-by-object-reference).
When you assign an object to a variable, Ruby stores a reference to that object in the variable. Assignment does not normally copy the object:
first = ["English"]
second = first
second << "Spanish"
p first # ["English", "Spanish"]
Both variables refer to the same array, so mutating it through second is visible through first.
The same idea applies here:
lang_errors = @user.errors
lang_errors and @user.errors refer to the same ActiveModel::Errors object at that moment. Rails validations treat this object as temporary state for the current validation attempt. When @user.save validates the record, Rails may clear previous errors and add new ones. Therefore, asking lang_errors for messages after save reads the changed errors object.
This is not Ruby "washing out" the variable. The variable still refers to an object; the object's contents were mutated by the validation process.
This matters whenever code stores mutable objects such as arrays, hashes, strings, model errors, configuration hashes, or service objects and expects their old contents to remain unchanged.
Mental Model
Think of a variable as a sticky note with a library shelf location written on it, not as a photocopy of the book.
lang_errors = @user.errors
places two sticky notes—lang_errors and @user.errors—on the same shelf. If Rails replaces pages in the book by clearing and recalculating errors during save, both sticky notes still lead to that now-updated book.
To keep the old wording, make a photocopy before Rails changes the book:
saved_messages = @user.errors.full_messages.dup
Now saved_messages is a separate array containing the messages that existed at that time.
Syntax and Examples
Variable assignment points another variable at the same object:
errors_a = @user.errors
errors_b = errors_a
puts errors_a.equal?(errors_b) # true
Use equal? when you want to check whether two expressions refer to the exact same object.
A mutable-object example:
languages = ["English"]
copy_name = languages
copy_name << "French"
p languages # ["English", "French"]
To keep a snapshot, create an independent object. For Rails error messages, copy the array returned by full_messages:
language_error_messages = @user.errors.full_messages.dup
@user.state = params[:user][:state]
@user.save
logger.debug language_error_messages.inspect
full_messages returns an array of display-ready strings. dup makes it explicit that this is a separate array intended to be retained.
If you need structured errors rather than display strings, copy the data you need before saving:
language_error_details = @user.errors.details.deep_dup
details is useful when code needs error symbols such as :blank or :invalid. Use full_messages when the application only needs text for logs or the UI.
Step by Step Execution
Consider this Rails-oriented flow:
@user.update_languages(nil, "French", nil)
saved_language_messages = @user.errors.full_messages.dup
# Example value: ["Language 1 can't be blank", "Language 3 can't be blank"]
@user.state = "active"
@user.save
p saved_language_messages
p @user.errors.full_messages
Step by step:
update_languagesruns and adds errors to@user.errors.@user.errors.full_messagescreates an array containing the current human-readable messages.dupcreates a separate array, stored insaved_language_messages.- Assigning
statechanges an attribute on@userbut does not itself validate the record. @user.saveruns validations. Rails can clear the model's existing errors and populate it with errors from this validation run.@user.errors.full_messagesnow represents the latest validation state.saved_language_messagesstill contains the earlier messages because it is a separate array.
Compare that with this code:
lang_errors = @user.errors
@user.save
p lang_errors.full_messages
No snapshot was made. lang_errors still points at the model's live errors object, so it reports the current errors after save.
Real World Use Cases
- Forms with multiple validation stages: Save errors from a custom check, then run normal model validation without losing the earlier messages needed by the response.
- Audit logging: Capture validation messages before retrying a save or applying fallback values.
- API error responses: Preserve errors from one operation while another operation runs validation on the same model instance.
- Background jobs: Copy a list of failed items before a retry mutates the original array or result object.
- Configuration handling: Duplicate a default hash before customizing it for one request, so other requests do not see the changes.
In each case, decide whether you need a live view of an object or a snapshot of its state at one point in time.
Real Codebase Usage
In Rails applications, developers usually avoid relying on errors from an earlier validation after another call that can validate the same record. Common patterns include:
Capture messages before another operation
@user.update_languages(language1, language2, language3)
language_messages = @user.errors.full_messages.dup
if @user.save
# success path
else
current_save_messages = @user.errors.full_messages
# handle both sets deliberately
end
Use separate validation contexts or separate objects
If language validation and profile persistence are independent operations, represent their outcomes independently rather than combining them accidentally in one model's temporary errors collection.
language_result = LanguageUpdater.new(@user, params[:language]).call
profile_saved = @user.update(user_params)
Add errors only after the final validation when appropriate
Custom validation logic is commonly placed in model validation methods so all errors are generated during the same save validation cycle:
validate :languages_are_valid
private
def languages_are_valid
errors.add(:languages, "must include a primary language") if primary_language.blank?
end
Prefer short-circuiting boolean operators for saves
If success starts as true, use && when a later save should not run after a failure:
success = success && @user.save
& evaluates both sides and is a bitwise/logical method operation. For booleans it returns a boolean, but it does not short-circuit.
Common Mistakes
Assuming assignment creates a copy
lang_errors = @user.errors
This creates another reference, not a copy. Capture data instead:
language_messages = @user.errors.full_messages.dup
Copying after calling save
@user.save
language_messages = @user.errors.full_messages.dup
This captures only the errors from the save validation attempt. Copy before save if earlier errors are the ones you need.
Using dup on the wrong level without knowing the goal
old_errors = @user.errors.dup
This may be useful in limited cases, but it is usually clearer and safer to copy the exact data the application needs, such as full_messages or details. An errors object can contain internal model-related state, while an array of messages is a straightforward snapshot.
Expecting validation errors to be permanent
ActiveModel::Errors represents the result of validation at a particular time. A successful validation can leave it empty; a later failed validation can populate it differently.
Using & when short-circuiting is expected
success = success & @user.save
Both operands are evaluated. If success is already false, @user.save still runs. Prefer this when you mean "only save if previous steps succeeded":
success = success && @user.save
Mutating a snapshot by accident
messages = @user.errors.full_messages
messages.clear
Even when a method returns a separate array, treat your stored snapshot as data you own. Use freeze if it should never be modified:
messages = @user.errors.full_messages.dup.freeze
Comparisons
| Situation | What happens | Use it when |
|---|---|---|
b = a | a and b refer to the same object | You intentionally want shared, live state |
b = a.dup | A shallow duplicate is made | You need a separate outer array, hash, or string |
b = a.clone | A duplicate is made and frozen state is generally preserved | You specifically need clone semantics |
messages = errors.full_messages.dup | A separate array of current display messages is kept | You need a readable error snapshot |
details = errors.details.deep_dup | Nested structured error data is copied | You need error codes and options later |
Ruby argument passing follows the same object-reference behavior as assignment:
def add_item(items)
items << "new" # Mutates the caller's array
items = ["other"] # Only rebinds this local parameter
end
values = ["original"]
add_item(values)
p values # ["original", "new"]
Ruby lets a method mutate a shared object, but assigning a new object to a parameter does not replace the caller's variable.
Cheat Sheet
# Assignment shares an object reference
b = a
b.equal?(a) # true
# Make a shallow copy
copy = array.dup
copy = hash.dup
copy = string.dup
# Keep Rails validation messages before another validation
messages = @user.errors.full_messages.dup
# Keep structured Rails error information
error_details = @user.errors.details.deep_dup
# `save` may clear and rebuild `@user.errors`
@user.save
# Short-circuit a later operation when earlier work failed
success = success && @user.save
Rules:
- Assignment does not copy objects.
- Mutations are visible through every reference to the same object.
- Reassignment changes only the variable being assigned.
- Copy a snapshot before an operation that may mutate the original.
dupis shallow; nested mutable objects may still be shared. Usedeep_dupwhen nested data must be independent.
FAQ
Is Ruby pass-by-reference or pass-by-value?
Ruby passes object references by value. In everyday Ruby discussions, "pass-by-object-reference" or "object sharing" is often the clearest description: a method receives a reference to the same object, but cannot reassign the caller's variable.
Why does lang_errors change after @user.save?
lang_errors refers to the same mutable errors object as @user.errors. Calling save runs validations, and Rails updates that errors object.
How do I save ActiveRecord errors before calling save?
For display text, use:
saved_messages = @user.errors.full_messages.dup
Do this before the later call to save.
Should I use @user.errors.dup?
Usually, copy the specific information you need instead. full_messages.dup is best for messages, and details.deep_dup is better for structured error data.
Does dup make a fully independent deep copy?
No. dup is usually shallow. If an array or hash contains nested mutable objects that must also be independent, use Rails deep_dup or explicitly copy the nested values.
Why are my errors empty after a successful save?
Errors describe the latest validation result. A successful validation normally has no errors, so Rails clears earlier validation failures.
What is the difference between & and && with booleans?
&& short-circuits: it skips the right side if the left side is false. & evaluates both sides. For chaining operations such as saves, && is typically the intended operator.
Mini Project
Description
Build a small profile update workflow that validates languages first, preserves those messages, and then attempts a profile save. The project demonstrates the difference between a live errors object and a copied error-message snapshot.
Goal
Capture language validation messages before a later validation can change the model's errors collection.
Requirements
- Create a
Userclass with a mutableerrorsarray. - Add a method that validates three language inputs.
- Capture the current error messages before calling
save. - Make
saveclear previous errors and validate the user's state. - Print both the saved language-message snapshot and the current errors after saving.
Keep learning
Related questions
Add a Default Value to an Existing Column in a Rails Migration
Learn how to add or change a default value on an existing database column using Rails Active Record migrations safely.
Calling a Class Method from an Instance in Ruby
Learn how to call a class method from an instance in Ruby using self.class, with examples, pitfalls, and practical usage patterns.
Calling an Overridden Monkey-Patched Method in Ruby
Learn how to call the original method when monkey patching in Ruby, including alias_method patterns, examples, pitfalls, and practical usage.