skip to content

In Ruby 4.0, why can require "observer" fail in a Bundler app, and how does the Observable module notify its observers?

level: middleimportance: nice to knowfreq 12%

answer

  1. no longer a default gem
  2. bundled gem since Ruby 3.4
  3. add it to the Gemfile
  4. changed before notify_observers
  5. observers need an update method

basics

~20 s

observer became a bundled gem in Ruby 3.4, so under Bundler require "observer" raises LoadError unless the Gemfile lists it. Observable's notify_observers calls update on each registered observer, but only after changed has been called.

solid answer

~40 s

Until Ruby 3.3 `observer` was a default gem, loadable anywhere. Ruby 3.3 warned when it was required without a Gemfile entry, and Ruby 3.4 made it a **bundled** gem: it still ships with Ruby and loads in a plain script, but under Bundler `require "observer"` raises `LoadError`, right after Ruby's require hook prints a note that it is not part of the default gems since Ruby 3.4.0 and a hint to add it to the Gemfile or gemspec. Adding `gem "observer"` fixes it. The API is small: a class does `include Observable`; `add_observer(obj)` registers an object and raises `NoMethodError` if it does not respond to `update`; `changed` marks the state as changed; `notify_observers(*args)` calls `update(*args)` on each observer only when `changed` was called; `count_observers` and `delete_observer` manage the list.

code

ruby · 21 lines
ruby
# Gemfile needs: gem "observer"
require "observer"

class Profile
  include Observable
  attr_reader :name

  def rename(new_name)
    @name = new_name
    changed
    notify_observers(self)
  end
end

class SearchIndexer
  def update(profile) = puts("reindex #{profile.name}")
end

profile = Profile.new
profile.add_observer(SearchIndexer.new)
profile.rename("Ada") # prints "reindex Ada"

go deeper

for a junior

Remember that observer must be in the Gemfile on Ruby 3.4 and later, and that changed has to be called before notify_observers.

for a middle

Explain default versus bundled gems, why Bundler raises LoadError, and the add_observer, changed, notify_observers and update contract.

for a senior

During a Ruby upgrade, find requires of bundled gems such as observer before they fail in production, and add them to the Gemfile deliberately.

for a principal

Plan upgrades around the default-to-bundled gem schedule so each Ruby bump comes with a reviewed list of Gemfile additions.

## Default gems and bundled gems Ruby ships part of its standard library as gems, in two tiers: | Tier | Ships with Ruby | Under Bundler | |---|---|---| | **Default gem** (for example `forwardable`, `delegate`, `singleton`) | yes, cannot be uninstalled | loadable without a Gemfile entry | | **Bundled gem** (for example `observer`, `csv`, `base64`) | yes, installed but removable | must be listed in the Gemfile or gemspec | Bundler restricts `require` to the gems in the bundle. A default gem is always available; a bundled gem is just another installed gem, so a Bundler app sees it only when the Gemfile names it. ## What changed for observer 1. **Ruby 3.3** listed `observer` among libraries that would become bundled gems, and warned on `require "observer"` in a Bundler app whose Gemfile did not include it. 2. **Ruby 3.4** moved `observer` (0.1.2) to the bundled gems, together with `csv`, `base64`, `bigdecimal`, `drb` and others. 3. **Ruby 4.0** still ships `observer` 0.1.2 as a bundled gem. In a Ruby 4.0 app started with `bundle exec`, `require "observer"` without a Gemfile entry raises **`LoadError`** ("cannot load such file"). Just before that, the require hook Ruby installs under Bundler prints an explanation: the library "used to be loaded from the standard library, but is not part of the default gems since Ruby 3.4.0", followed by "You can add observer to your Gemfile or gemspec to fix this error." A plain `ruby script.rb` outside Bundler still loads it, because the bundled gem is installed with Ruby. The fix is one line in the Gemfile: ```ruby gem "observer" ``` ## The Observable API `observer` defines the module **`Observable`**, which a class includes to become a **subject** that other objects watch: - `add_observer(obj)` registers `obj`; it raises `NoMethodError` at registration time if `obj` does not respond to `update`. Adding the same observer twice keeps one entry. - `changed` sets the subject's changed flag. - `notify_observers(*args)` calls `obj.update(*args)` on every registered observer, **but only if `changed` was called** first; otherwise it does nothing. - `count_observers` returns how many unique observers are registered. - `delete_observer(obj)` and `delete_observers` remove one or all. Notification is synchronous and in-process: `notify_observers` returns after every `update` has run, on the calling thread. ## A worked example A user profile that tells a search indexer about changes: 1. `class Profile` does `include Observable`. 2. The indexer defines `update(profile)`. 3. `profile.add_observer(indexer)` registers it. 4. When the profile changes, it calls `changed` and then `notify_observers(self)`. Forgetting `changed` is the classic bug: the code runs, nothing is notified, and no error is raised. ## Finding affected requires before an upgrade The `observer` change is one entry in a schedule. Ruby keeps the list in `lib/bundled_gems.rb`, a `SINCE` table that records the release in which each library stopped being a default gem, such as 3.4.0 for `observer`, `csv` and `base64`, or 4.0.0 for `ostruct` and `logger`. Before an upgrade: 1. Search the code base for `require` of each library on that list. 2. Run the test suite on the previous Ruby with Bundler; the hook prints a warning for each library that will move. 3. Add the gems you really use to the Gemfile, with a version constraint, in the same change as the Ruby bump. ## When to reach for it - Small in-process notifications where a hand-written list of callbacks would do the same job. - Code already using it that must keep working on Ruby 3.4 and 4.0: add the Gemfile line during the upgrade. Many teams instead keep an Array of callables or use an event library; the choice between those designs belongs to the pattern, not to this gem.

  • Why does the same require "observer" work in ruby script.rb but fail under bundle exec on Ruby 4.0?
    Outside Bundler, RubyGems can activate any installed gem, and bundled gems are installed with Ruby. Under Bundler only gems in the Gemfile are loadable, and since Ruby 3.4 observer is a bundled gem rather than a default gem, so it must be listed.
  • What does add_observer do with an object that has no update method?
    It raises `NoMethodError` at registration time instead of accepting the object and failing on the first notification, which surfaces the mistake where it was made.

saying these in an interview costs you the question

  • observer is still a default gem in Ruby 4.0
  • notify_observers calls update even if changed was never called
  • A bundled gem loads under Bundler without a Gemfile entry
  • Ruby 4.0 only warns when observer is missing from the Gemfile
  • add_observer accepts any object and fails later at notify time