ruby-concurrency/concurrent-ruby · error · ArgumentError

struct size differs

Error message

struct size differs

What it means

The generated struct class's ns_initialize accepts at most as many positional values as there are members; a longer argument list raises ArgumentError('struct size differs'). Fewer values are padded with nil, so only the upper bound is enforced. This is the concurrent structs' equivalent of core Struct's arity check.

Source

Thrown at lib/concurrent-ruby/concurrent/synchronization/abstract_struct.rb:146

      # @!visibility private
      def pr_underscore(clazz)
        word = clazz.to_s.dup # dup string to workaround JRuby 9.2.0.0 bug https://github.com/jruby/jruby/issues/5229
        word.gsub!(/::/, '/')
        word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
        word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
        word.tr!("-", "_")
        word.downcase!
        word
      end

      # @!visibility private
      def self.define_struct_class(parent, base, name, members, &block)
        clazz = Class.new(base || Object) do
          include parent
          self.const_set(:MEMBERS, members.collect{|member| member.to_s.to_sym}.freeze)
          def ns_initialize(*values)
            raise ArgumentError.new('struct size differs') if values.length > length
            @values = values.fill(nil, values.length..length-1)
          end
        end
        unless name.nil?
          begin
            parent.send :remove_const, name if parent.const_defined?(name, false)
            parent.const_set(name, clazz)
            clazz
          rescue NameError
            raise NameError.new("identifier #{name} needs to be constant")
          end
        end
        members.each_with_index do |member, index|
          clazz.send :remove_method, member if clazz.instance_methods(false).include? member
          clazz.send(:define_method, member) do
            @values[index]
          end
        end

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Validate before splatting: raise ArgumentError, "expected <= #{K.members.size} values" if arr.length > K.members.size
  2. If truncation is acceptable by design, slice explicitly: K.new(*arr.first(K.members.size))
  3. After changing a member list, update every construction site of that struct class

Example fix

# before
Point = Concurrent::MutableStruct.new(:x, :y)
pt = Point.new(*csv_row) # raises when csv_row has 3+ columns

# after
raise ArgumentError, 'row too long' if csv_row.length > Point.members.size
pt = Point.new(*csv_row.first(Point.members.size))
Defensive patterns

Strategy: validation

Validate before calling

max = Klass.members.size
raise ArgumentError, "#{values.length} values for #{max} members" if values.length > max
Klass.new(*values)

Try / catch

begin
  Klass.new(*values)
rescue ArgumentError => e
  raise unless e.message == 'struct size differs'
  Klass.new(*values.first(Klass.members.size)) # explicit truncation policy
end

Prevention

When it happens

Trigger: Point.new(1, 2, 3) for Point defined with 2 members; K.new(*arr) where arr outgrew the member list after a data change; passing a defaulted splat whose length varies per caller.

Common situations: A struct definition is trimmed (member removed) while some constructors still pass the old arity; splatting external data (CSV rows, JSON arrays) without length validation; merging two structs' constructors during a refactor.

Related errors


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