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.newView on GitHub (pinned to 0b88d5ff75)
Solutions
- Pass the body as a block: Concurrent.atomically { account.value -= 10 }
- When forwarding from a method, capture and re-attach: def transfer(&blk); Concurrent.atomically(&blk); end
- 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
- Forward blocks explicitly with &blk in STM wrapper methods
- Fail fast in builders when the transaction proc is nil
- Prefer Concurrent.atomically { ... } with a literal block at call sites
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.