Exceptions and Observers (2016)


Observers are a useful mechanism to set up event flows. They are designed for 'fan out' communication, in which multiple objects are notified when a single object changes. Their power lies in their use of composition, allowing observers to be added and removed at runtime as necessary, based on the current state of the application.

Many languages have implementations of the observer pattern. Ruby, for example, has a module Observable that implements the pattern. The documentation is straightforward enough:

  1. include Observable
  2. Have observers implement an update method
  3. Have observers register with the source via add_observer
  4. Have the source call changed and then notify_observers

Standard Implementation

So how does this look in code? Here's a simple event source that we might want to observe:

require 'observer'

class Source
  include Observable
  def ping(int)

When Source is has ping called on it, it will let its observers know. A simple observer might just print out the value:

class Sink
  def update(int)

Since observers use composition, the objects are wired up at runtime:

source = Source.new
source.ping(3) # nothing happens
# Now add an observer
sink = Sink.new
source.ping(7) # prints '7'

This structure is perfect when you have a system that has events flowing into it, and you want to have multiple, unrelated subsystems respond to those events. Each subsystem can register and unregister listeners as needed, allowing the program to dynamically reconfigure itself as needed at runtime.

When Thing Go Wrong

In a standard deployment of the observer pattern, a given source can have several observers, and they most frequently know very little about each other. Each observer will register at a time of its choosing, largely independently of every other observer. This means that observers know nothing about in what order they have registered with the source, or even how many other observers there are.

In almost every discussion of observers, there is no discussion about what happens when an exception is thrown in an observer's update method. What should happen? Let's look at Ruby again and add another observer.

class SnarkySink
  def update(int)
    fail 'I aim to misbehave'

SnarkySink is not something we'd ever write in production code, but exceptions are especially common in observers that write to a file or push data over the network, since I/O is error-prone. What happens when SnarkySink is listening to Source?

source = Source.new
source.ping(3) # nothing happens

snarky_sink = SnarkySink.new

sink = Sink.new

source.ping(7) # Throws exception!

There are two terrible things that happened here.

Observers Break the Source Contract

Responsible code will state clearly what its behavior is given valid input. If a method has an error state, it should be called out so clients can handle it and take the appropriate action. If clients don't have the context to handle an exception, it shouldn't be raised, since nothing up the call stack knows what to do with it. Exceptions need to be handled at whatever level in the call stack is most able to recover from them.

Instead, we have a method ping, which has no reason to raise any errors so clients call it with valid input and expect it to handle whatever exceptions might arise. All is well until SnarkySink comes along, registers as an observable with Source, and suddenly ping is raising a RuntimeError that makes no sense. This alters the contract of ping and breaking clients that call it.

And that's not even the bad part.

Observers Block Event Propagation

The bad part is that Sink#update never got called at all because Ruby's implementation of notify_observers blindly iterates through each observer in order and calls it, passing whatever exception is encountered right back to caller and stopping iteration over the registered observers.

This behavior breaks the fundamental contract of the observer pattern, which is to call observers when the state has changed. Worse, whether the contract is respected is dependent on the order in which the observers register, which can vary not only run-to-run, but dynamically during runtime.

Class Invariants

The fundamental value proposition of object-oriented programming is encapsulation. It comes in many forms, like Law of Demeter and data hiding. At the most broad level, classes should state what invariants they guarantee and then enforce those invariants, never letting other objects in the system observe them in an invalid state.

As pointed out (in bold) on C2:

Before throwing an exception, a method should return the object to a state that meets its class invariants.

Indeed, C2 has a small page dedicated to exactly the issue of Observers and Exceptions, which is more than Wikipedia, the Java documentation, or the Ruby documentation dedicate to the issue.

In the case of an observable object, a class invariant is that the observers will be called when the state being observed changes. Ruby's implementation fundamentally fails to maintain this invariant, instead delegating it to whatever classes include the Observable module.

Maintaining the Contract

It turns out that fixing this is (mostly) fairly straightforward.

class RobustSink
  def update(data)
  rescue StandardError => e
    puts("#{self.class.name} encountered error while processing #{data}: #{e}")

  def do_update
    fail NotImplementedError "#{self.class.name} must implement #{__method__}"

Every observer can now inherit from RobustSink, and implement a do_update method. This would be simple enough to include in Ruby's Observable module, or perhaps even a dedicated Observers module.

This implementation puts the responsibility to maintain the invariant on each observer. There are arguments against this, but it does allow each Observer to trap and handle the sorts of exceptions unique to that particular observer, most likely completely unknown to the object being observed. As pointed out on C2:

A key tenet is that the observed does not know anything about the observers. It "publishes" a change and the observers get notified of the change.