minitest/minitest · error · TypeError

class or module required for rescue clause. Got %p

Error message

class or module required for rescue clause. Got %p

What it means

Minitest raises this TypeError from assert_raises when the expected-exception arguments are not Ruby classes/modules. After popping a trailing String as the custom failure message and defaulting to StandardError when no class was given, assert_raises verifies 'exp.all? Module' (assertions.rb:411) before doing 'rescue *exp'; any non-Module value left in the list aborts with this message instead of a confusing error from rescue itself.

Source

Thrown at lib/minitest/assertions.rb:411

    #   assert_raises(CustomError, 'This should have raised CustomError') { method_with_custom_error }
    #
    # Using the returned object:
    #
    #   error = assert_raises(CustomError) do
    #     raise CustomError, 'This is really bad'
    #   end
    #
    #   assert_equal 'This is really bad', error.message

    def assert_raises *exp
      flunk "assert_raises requires a block to capture errors." unless
        block_given?

      msg = "#{exp.pop}.\n" if String === exp.last
      exp << StandardError if exp.empty?

      # TODO: remove this if https://bugs.ruby-lang.org/issues/22007 gets fixed
      raise TypeError, NO_RE_MSG % [exp] unless exp.all? Module

      begin
        yield
      rescue *exp => e
        pass # count assertion
        return e
      rescue Minitest::Assertion # incl Skip & UnexpectedError
        # don't count assertion
        raise
      rescue SignalException, SystemExit
        raise
      rescue Exception => e
        flunk proc {
          exception_details(e, "#{msg}#{mu_pp exp} exception expected, not")
        }
      end

      exp = exp.first if exp.size == 1

View on GitHub (pinned to 581e7d5386)

Solutions

  1. Pass exception classes, not instances or strings: assert_raises(ArgumentError) { ... }
  2. To assert on the message, use the returned exception: err = assert_raises(RuntimeError) { ... }; assert_equal "boom", err.message
  3. If the class comes from a variable, guard it: raise ArgumentError, "#{exp.inspect} is not an exception class" unless exp.is_a?(Class)
  4. Remember the argument contract: one or more exception classes plus one optional trailing String message, e.g. assert_raises(CustomError, 'This should have raised CustomError') { ... }

Example fix

# before
assert_raises(RuntimeError.new) { risky_call }         # => TypeError
assert_raises("connection failed", "note") { risky }  # => TypeError

# after
error = assert_raises(RuntimeError) { risky_call }
assert_equal "connection failed", error.message
Defensive patterns

Strategy: validation

Validate before calling

# validate assert_raises args the way minitest does (assertions.rb:407-411):
def assertable? *exp
  exp = exp.dup
  exp.pop if String === exp.last      # trailing string = custom message
  exp << StandardError if exp.empty?  # default
  exp.all? { |e| e.is_a?(Module) }
end

raise ArgumentError, 'pass exception classes' unless assertable?(MyError, 'msg')

Type guard

# returns true only when every arg (minus a trailing message string) is a class/module
def assert_raises_args_valid?(*exp)
  exp = exp.dup
  exp.pop if String === exp.last
  exp << StandardError if exp.empty?
  exp.all? { |e| e.is_a?(Module) }
end

Try / catch

begin
  assert_raises(*expected) { risky }
rescue TypeError => e
  raise ArgumentError, "assert_raises needs exception classes, got #{expected.inspect}" unless e.message =~ /class or module required/
  raise
end

Prevention

When it happens

Trigger: Calling assert_raises with an exception instance: assert_raises(RuntimeError.new) { ... }; with a symbol: assert_raises(:timeout) { ... }; with a nil or interpolated variable: assert_raises(@error) { ... } where @error is nil; with a string that is not the LAST argument: assert_raises("boom", "note") { ... } — only the final String is consumed as the failure message, so an earlier string stays in the rescue list.

Common situations: Habit carried over from RSpec's raise_error("message text"), which accepts message strings; passing a dynamically loaded error constant that turns out nil; copy-pasting the error message instead of the error class into assert_raises.

Understand the failure class

Background: Invalid argument type errors: "must be of type string", "expected X, got Y", and ERR_INVALID_ARG_TYPE explained — this error's family across 15 libraries.

Related errors


AI-assisted analysis of minitest/minitest@581e7d5386 (2026-08-23). Data as JSON: /api/errors/effc8690e26efb2b. Report an issue: GitHub.