DEV Community

Cover image for Getting to Know Yourself: Understanding self in Ruby

Getting to Know Yourself: Understanding self in Ruby

Sina Rezaei on October 02, 2026

self is one of those Ruby keywords that looks easy until you meet it in real code. You see it in a setter: self.name = normalized_name ...
Collapse
 
pawel_nowak profile image
Pawel •

The setter case is the one that actually bit me in a Rails model. I had a callback doing status = "paid" and spent an embarrassing amount of time wondering why nothing persisted, because of course it just created a local variable. After that, I started treating every bare assignment inside an instance method with suspicion. The scope gate framing is nice too, since it explains why the block case feels inconsistent when it really is not.

Collapse
 
sinarezaei profile image
Sina Rezaei • • Edited

Yeah, that status = "paid" case is a nasty one because it looks completely reasonable at a glance. Ruby is basically saying “sure, that’s a local variable” while you’re thinking “no, update the model.”

A similar thing shows up when you have a setter and a method with the same name:

class Order
  attr_accessor :status

  def mark_as_paid
    status = "paid"       # local variable
    self.status = "paid"  # calls status=
  end
end
Enter fullscreen mode Exit fullscreen mode

The interesting part is that the setter itself isn’t the weird bit. The missing receiver is. Ruby needs self there to know you mean the assignment method rather than creating a local. That’s also why I like the scope-gate framing. Once you start looking for the receiver and the current scope instead of memorizing Ruby’s “exceptions,” these cases become much easier to reason about.

Collapse
 
pawel_nowak profile image
Pawel •

That minimal repro is going straight into my notes. You put it better than I did: the setter isn't the weird bit, the missing receiver is. Once you read self as a message send instead of boilerplate, the behavior stops feeling arbitrary.

Thread Thread
 
sinarezaei profile image
Sina Rezaei •

Exactly. And I think that distinction becomes even more useful once you hit Rails code that mixes implicit receivers with DSL-style APIs.

For example, in an ActiveRecord model, update and update! are explicit message sends, while something like status = "paid" is parsed as local assignment before Ruby ever gets to the question of which object should receive a message. That difference can become surprisingly important when a callback or service object is doing several things that look like ordinary Ruby expressions.

Once you start reading self as the receiver of a message, these Rails “magic” moments become much less magical. You can usually trace the behavior back to ordinary Ruby parsing and method dispatch.

Thread Thread
 
pawel_nowak profile image
Pawel •

Good point. The update(...) case is a nice anchor: it is clearly a message to the instance, so it steadies the mental model before you hit the ambiguous assignment. I still double-take on bare assignment inside callbacks now and then, even after years of this.

Thread Thread
 
sinarezaei profile image
Sina Rezaei •

Yeah, and that’s probably why I think the “receiver” model holds up better than memorizing Ruby syntax rules.

What gets interesting in Rails is that DSLs can deliberately make the receiver less obvious. Take something like:

class Order < ApplicationRecord
  enum :status, %i[pending paid failed]

  after_commit :publish_event, on: :update

  def publish_event
    OrderEvent.create!(
      order: self,
      status: status,
      changes: saved_changes
    )
  end
end
Enter fullscreen mode Exit fullscreen mode

Inside publish_event, status is an implicit message to the current self, while OrderEvent.create! has an explicit receiver. Then saved_changes is another implicit message, but its meaning depends entirely on what object self is at that point.

That’s where Rails can make the same Ruby rule feel more complicated than it actually is. The framework adds a lot of DSL surface area, but the underlying question is still pretty mechanical: who is receiving this message right now?

Once that becomes the first question you ask, callbacks, scopes, validations, and model DSLs start feeling a lot less magical.