ruby-concurrency/concurrent-ruby · error · ArgumentError

no block given

Error message

no block given

What it means

Concurrent.atomically (or Concurrent::TVar-based code) runs a software-transactional-memory transaction and requires a block describing the reads/writes of TVars. Calling it without a block raises ArgumentError('no block given') before any transaction starts. Nested calls are flattened into the outer transaction, so the block is the only way to express the transactional body.

Source

Thrown at lib/concurrent-ruby/concurrent/tvar.rb:83

  # * If an exception escapes an atomically block it will abort the transaction.
  #
  # * It is undefined behaviour to use callcc or Fiber with atomically.
  #
  # * If you create a new thread within an atomically, it will not be part of
  #     the transaction. Creating a thread counts as a side-effect.
  #
  # Transactions within transactions are flattened to a single transaction.
  #
  # @example
  #   a = new TVar(100_000)
  #   b = new TVar(100)
  #
  #   Concurrent::atomically do
  #     a.value -= 10
  #     b.value += 10
  #   end
  def atomically
    raise ArgumentError.new('no block given') unless block_given?

    # Get the current transaction

    transaction = Transaction::current

    # Are we not already in a transaction (not nested)?

    if transaction.nil?
      # New transaction

      begin
        # Retry loop

        loop do

          # Create a new transaction

          transaction = Transaction.new

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Pass the body as a block: Concurrent.atomically { account.value -= 10 }
  2. When forwarding from a method, capture and re-attach: def transfer(&blk); Concurrent.atomically(&blk); end
  3. Validate at your API boundary: raise ArgumentError, 'transaction body required' unless block_given?

Example fix

# before
def transfer(from, to, amount)
  Concurrent.atomically # nothing to run
end

# after
def transfer(from, to, amount)
  Concurrent.atomically do
    from.value -= amount
    to.value += amount
  end
end
Defensive patterns

Strategy: validation

Validate before calling

def stm(&block)
  raise ArgumentError, 'transaction body block required' unless block
  Concurrent.atomically(&block)
end

Type guard

def transaction_body?(obj)
  obj.respond_to?(:call)
end

Try / catch

begin
  Concurrent.atomically { mutate_tvrs }
rescue ArgumentError => e
  raise unless e.message == 'no block given'
  raise ConfigError, 'transaction body missing for transfer'
end

Prevention

When it happens

Trigger: Concurrent.atomically with no block; forwarding a transaction body stored in a variable without █ guard clauses that conditionally skip attaching the block (e.g. 'if in_transaction, run outside STM' implemented by dropping the block).

Common situations: Wrapping STM transactions in helper methods and losing the & forwarding; DSL builders that assemble transactions dynamically and end up with nil bodies; refactors from ivar mutation to TVar that leave call sites blockless.

Related errors


AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21). Data as JSON: /api/errors/f6a7e5c378286038. Report an issue: GitHub.