ruby-concurrency/concurrent-ruby · error · TypeError

can't dump hash with default proc

Error message

can't dump hash with default proc

What it means

Concurrent::Map implements Marshal support via marshal_dump (map.rb:304-309). If the Map was created with a default proc (Concurrent::Map.new { |m, k| ... }), the proc cannot be serialized, so marshal_dump proactively raises TypeError ("can't dump hash with default proc"), matching Ruby's Hash behavior. The entries are not the problem; the unserializable proc is.

Source

Thrown at lib/concurrent-ruby/concurrent/map.rb:306

    # Is map empty?
    # @return [true, false]
    def empty?
      each_pair { |k, v| return false }
      true
    end unless method_defined?(:empty?)

    # The size of map.
    # @return [Integer] size
    def size
      count = 0
      each_pair { |k, v| count += 1 }
      count
    end unless method_defined?(:size)

    # @!visibility private
    def marshal_dump
      raise TypeError, "can't dump hash with default proc" if @default_proc
      h = {}
      each_pair { |k, v| h[k] = v }
      h
    end

    # @!visibility private
    def marshal_load(hash)
      initialize
      populate_from(hash)
    end

    undef :freeze

    # @!visibility private
    def inspect
      format '%s entries=%d default_proc=%s>', to_s[0..-2], size.to_s, @default_proc.inspect
    end

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Do not build the Map with a default proc: create it plain and read with map.fetch(k) { default }.
  2. Convert to a plain Hash before dumping: map.each_pair.with_object({}) { |(k, v), h| h[k] = v }.
  3. If the default proc is required, exclude the Map from Marshal payloads and persist only its entries.

Example fix

# before
stats = Concurrent::Map.new { |m, k| m[k] = 0 }
Marshal.dump(stats) # TypeError: can't dump hash with default proc

# after
stats = Concurrent::Map.new
stats.fetch(k) { stats[k] = 0 }
Marshal.dump(stats.each_pair.with_object({}) { |(k, v), h| h[k] = v })
Defensive patterns

Strategy: validation

Validate before calling

# Track whether the Map carries a default proc and dump entries, not the Map
has_default_proc = true/false # known at construction time
payload = has_default_proc ? map.each_pair.with_object({}) { |(k, v), h| h[k] = v } : map
Marshal.dump(payload)

Try / catch

begin
  Marshal.dump(map)
rescue TypeError
  Marshal.dump(map.each_pair.with_object({}) { |(k, v), h| h[k] = v })
end

Prevention

When it happens

Trigger: Marshal.dump(Concurrent::Map.new { |m, k| 0 }); a Map created with a default_proc passed through Marshal.dump, DRb, or fork-based IPC; a caching layer (Rails cache, Sidekiq job arguments) serializing an object graph that contains a default-proc Map.

Common situations: Counter/memoization maps built with default-proc blocks and then shipped to a background job; deep state objects containing a Concurrent::Map dumped for persistence; DRb distribution of shared state.

Related errors


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