Question

Ruby Object References and Mutable ActiveRecord Errors

rubyruby-on-railspass-by-reference

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.

Take Quiz

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:

  1. update_languages runs and adds errors to @user.errors.
  2. @user.errors.full_messages creates an array containing the current human-readable messages.
  3. dup creates a separate array, stored in saved_language_messages.
  4. Assigning state changes an attribute on @user but does not itself validate the record.
  5. @user.save runs validations. Rails can clear the model's existing errors and populate it with errors from this validation run.
  6. @user.errors.full_messages now represents the latest validation state.
  7. saved_language_messages still 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.

Take Quiz

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

SituationWhat happensUse it when
b = aa and b refer to the same objectYou intentionally want shared, live state
b = a.dupA shallow duplicate is madeYou need a separate outer array, hash, or string
b = a.cloneA duplicate is made and frozen state is generally preservedYou specifically need clone semantics
messages = errors.full_messages.dupA separate array of current display messages is keptYou need a readable error snapshot
details = errors.details.deep_dupNested structured error data is copiedYou 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.

Take Quiz

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.
  • dup is shallow; nested mutable objects may still be shared. Use deep_dup when 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.

Take Quiz

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

  1. Create a User class with a mutable errors array.
  2. Add a method that validates three language inputs.
  3. Capture the current error messages before calling save.
  4. Make save clear previous errors and validate the user's state.
  5. Print both the saved language-message snapshot and the current errors after saving.
Take Quiz

Keep learning

Related questions

View more