Dear Russian friends, please watch President Zelenskyy's speech addressed to you. πŸ‡ΊπŸ‡¦Help our brave mates in Ukraine with a donation.


State is a mutation effect. It allows reading and writing non-local values.

Basic usage

Handling code:

require 'dry/effects'

class CountCalls
  include Dry::Effects::Handler.State(:counter)

  def call
    counter, result = with_counter(0) do

    puts "Counter: #{counter}"


Using code:

require 'dry/effects'

class HeavyLifting
  include Dry::Effects.State(:counter)

  def call
    self.counter += 1
    # ... do heavy work ...

Now it's simple to count calls by gluing two pieces:

count_calls =
heavy_lifting =

count_calls.() { 1000.times { heavy_lifting.() }; :done }
# Counter: 1000
# => :done

Handler interface

As shown above, the State handler returns two values: the accumulated state and the return value of the block:

include Dry::Effects::Handler.State(:state)

def run(&block)
  accumulated_state, result = with_state(initial_state) do

  # result holds the return value of

  # accumulated_state refers to the last written value
  # or initial_value if the state wasn't changed

Identifiers and mixing states

All state handlers and effects have an identifier. Effects with different identifiers are compatible without limitations but swapping the handlers may change the result:

require 'dry/effects'

class Program
  include Dry::Effects.State(:sum)
  include Dry::Effects.State(:product)

  def call
    1.upto(10) do |i|
      self.sum += i
      self.product *= i


program =

extend Dry::Effects::Handler.State(:sum)
extend Dry::Effects::Handler.State(:product)

with_sum(0) { with_product(1) { } }
# => [55, [3628800, :done]]
with_product(1) { with_sum(0) { } }
# => [3628800, [55, :done]]

Relation to Reader

A State handler eliminates Reader effects with the same identifier:

require 'dry/effects'

extend Dry::Effects::Handler.State(:counter)
extend Dry::Effects.Reader(:counter)

with_counter(100) { "Counter value is #{counter}" }
# => [100, "Counter values is 100"]

Not providing an initial value

There are cases when an initial value cannot be provided. You can skip the initial value but in this case, reading it before writing will raise an error:

extend Dry::Effects::Handler.State(:user)
extend Dry::Effects.State(:user)

with_user { user }
# => Dry::Effects::Errors::UndefinedStateError (+user+ is not defined, you need to assign it first by using a writer, passing initial value to the handler, or providing a fallback value)

with_user do
  self.user = 'John'

  "Hello, #{user}"
# => ["John", "Hello, John"]

One example is testing middleware without mutating env:

RSpec.describe AddingSomeMiddleware do
  include Dry::Effects::Handler.State(:env)
  include Dry::Effects.State(:env)

  let(:app) do
    lambda do |env|
      self.env = env
      [200, {}, ["ok"]]

  subject(:middleware) { }

  it 'adds SOME_KEY to env' do
    captured_env, _ = middleware.({})

    expect(captured_env).to have_key('SOME_KEY')

Default value for Reader effects

When no initial value is given, you can use a block for providing a default value:

extend Dry::Effects::Handler.State(:artist)
extend Dry::Effects.State(:artist)

with_artist { artist { 'Unknown Artist' } } # => "Unknown Artist"

When to use?

State is a classic example of an effect. However, using it often can make your code harder to follow.

octocatEdit on GitHub