puppetlabs/puppet · error · ArgumentError

%{klass} cannot be added to a Timespan

Error message

%{klass} cannot be added to a Timespan

What it means

Puppet::Pops::Time::Timespan#+ only accepts a Timestamp, a Timespan, or a plain Integer/Float (interpreted as seconds). Any other operand class falls into the else branch and raises ArgumentError with the operand's class name interpolated. Integer/Float operands are converted with (o * NSECS_PER_SEC).to_i, i.e. whole seconds, not nanoseconds.

Source

Thrown at lib/puppet/pops/time/timespan.rb:138

      format.parse(str)
    end

    # @return [true] if the stored value is negative
    def negative?
      @nsecs < 0
    end

    def +(o)
      case o
      when Timestamp
        Timestamp.new(@nsecs + o.nsecs)
      when Timespan
        Timespan.new(@nsecs + o.nsecs)
      when Integer, Float
        # Add seconds
        Timespan.new(@nsecs + (o * NSECS_PER_SEC).to_i)
      else
        raise ArgumentError, _("%{klass} cannot be added to a Timespan") % { klass: a_an_uc(o) } unless o.is_a?(Timespan)
      end
    end

    def -(o)
      case o
      when Timespan
        Timespan.new(@nsecs - o.nsecs)
      when Integer, Float
        # Subtract seconds
        Timespan.new(@nsecs - (o * NSECS_PER_SEC).to_i)
      else
        raise ArgumentError, _("%{klass} cannot be subtracted from a Timespan") % { klass: a_an_uc(o) }
      end
    end

    def -@
      Timespan.new(-@nsecs)
    end

View on GitHub (pinned to e227c27540)

Solutions

  1. Convert numeric-looking strings before adding: ts + o.to_i or ts + Float(o) for fractional seconds
  2. If the operand is a duration string like '1h30m', parse it: Timespan.parse(o) or construct Timespan.from_hash({'hours' => 30})
  3. Guard the call site with a case/is_a? check on Timestamp, Timespan, Integer, Float and raise a domain-specific error otherwise
  4. nil almost always means an upstream lookup returned nothing - check for nil before the expression rather than rescuing after

Example fix

// before
duration = ts + lookup('max_age')   # lookup returns a String like "3600"

// after
raw = lookup('max_age')
duration = ts + (raw.is_a?(String) ? raw.to_i : raw)
Defensive patterns

Strategy: type-guard

Validate before calling

allowed = [Puppet::Pops::Time::Timespan, Puppet::Pops::Time::Timestamp, Integer, Float]
raise ArgumentError, "bad addend #{o.inspect}" unless allowed.any? { |k| o.is_a?(k) }
duration = ts + (o.is_a?(String) ? o.to_i : o)

Type guard

def timespan_addable?(o)
  o.is_a?(Puppet::Pops::Time::Timespan) ||
    o.is_a?(Puppet::Pops::Time::Timestamp) ||
    (o.is_a?(Integer) || o.is_a?(Float))
end

Try / catch

begin
  ts + o
rescue ArgumentError => e
  raise DataError, "cannot add #{o.class} to Timespan (value: #{o.inspect})" 
end

Prevention

When it happens

Trigger: Calling ts + '5', ts + nil, ts + [1], ts + some_hash, or ts + a BigDecimal/Rational with a Puppet::Pops::Time::Timespan instance on the left. Also adding a value obtained from Puppet lookup/Hiera that arrived as a String.

Common situations: Puppet DSL or Ruby custom functions doing arithmetic on durations where one side came from external data (JSON/YAML gives Strings, not Integers); mixing Puppet's Timespan with Ruby Duration-like objects from other gems; passing a Puppet::Pops::Types::PScalarType value directly.

Related errors


AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21). Data as JSON: /api/errors/ee2aa9c6454c23e9. Report an issue: GitHub.