Skip to content

raise has four forms

Level: 201 · anyone who has only ever written raise "something went wrong"

One line: raise "msg" is a RuntimeError, raise Klass an instance whose message is the class name, raise Klass, "msg" and raise Klass.new("msg") the two explicit forms — and a bare raise inside a rescue re-raises the current exception, $!, while anything that is not an exception is a TypeError.

raise is not syntax; it is a method on Kernel, and its argument list is a small protocol. With a string it builds a RuntimeError. With a class it calls that class's exception method with no arguments, which by default is new, so the message is the class name. With a class and a string it calls Klass.exception("msg"). With an instance it raises that instance, and an instance plus a string raises a copy carrying the new message. A third argument replaces the backtrace — an Array of strings, most usefully caller, so an error is blamed on the line that called a validation method rather than the line inside it. With no argument inside a rescue clause it re-raises $!, the exception being handled; with no argument anywhere else there is nothing to re-raise and Ruby raises a RuntimeError with an empty message. fail is the same method under a second name. Two globals track the current exception: $! is the object and $@ is its backtrace, both set while a rescue clause runs and cleared after it.

Verified output of raise_has_four_forms_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

1. raise "msg" makes a RuntimeError
   RuntimeError: "just a message"
2. raise Klass makes an instance whose message is the class name
   ArgumentError: "ArgumentError"
3. raise Klass, "msg"
   ArgumentError: "with a message"
4. raise Klass.new("msg"); and raise obj, "msg" replaces the message
   ArgumentError: "an instance"
   ArgumentError: "replaced"
5. a bare raise inside rescue re-raises the same object
   outer got IOError: "inner"; same object: true
6. a bare raise outside any rescue: a RuntimeError with an empty message
   RuntimeError: ""
7. a third argument sets the backtrace
   e.backtrace ["frame_a", "frame_b"]
   with caller: the first entry is in "Object#validate", not in check
8. fail is an alias of raise
   RuntimeError: "fail is raise"
9. raising something that is not an exception
   TypeError: "exception class/object expected"
   TypeError: "exception class/object expected"
   TypeError: "exception class/object expected"
10. $! and $@ inside rescue, and after it
   $!.equal?(e)            true
   $@.equal?(e.backtrace)  true
   $! after the rescue     nil
11. raise Klass, args calls Klass.exception(*args)
   Custom.exception called with []
   Custom: "Custom"
   Custom.exception called with ["msg"]
   Custom: "msg"

Reading the output

Rows 1 to 4 are the four forms. In row 2 the message is "ArgumentError" — the class name — because ArgumentError.exception with no argument is ArgumentError.new with no argument, and Exception#message defaults to the class name. Row 4's second line shows the instance-plus-string form: the ArgumentError built with "original" reaches the handler saying "replaced", because raise obj, msg calls obj.exception(msg), which returns a copy with the new message.

Row 5 is the bare raise inside a rescue: the outer handler receives the very same object (equal? is true), not a copy or a wrapper, so the inner clause can log and step aside. Row 6 is the bare raise with nothing to re-raise: a RuntimeError whose message is the empty string. Row 7 shows the third argument — the two-element array becomes the backtrace verbatim, and with caller the first backtrace entry names Object#validate, the method that called check, rather than check itself, which is the point of passing it.

Row 8: fail is raise — same class, same message. Row 9 is what happens when the argument is not an exception: an Integer, a class that does not descend from Exception, and a string followed by a second string (there is no String.exception) all raise TypeError with the message exception class/object expected. Row 10 prints the two globals: inside the rescue, $! is the rescued object and $@ is its backtrace array; after the clause, $! is nil again. Row 11 makes the protocol visible: a class that overrides self.exception sees raise Custom arrive with no arguments and raise Custom, "msg" arrive with ["msg"] — this is the hook raise calls, and the custom exception lesson uses its instance-side twin.

Compared with Python

Python has one form — raise <instance> — and the shorthand raise Klass, which calls the constructor with no arguments (row 2: args=(), an empty tuple, not the class name). A string is not an exception, so row 1 is a TypeError in Python where it was the most common form in Ruby. The two-argument raise Klass, "msg" is Python 2 syntax and a SyntaxError in Python 3 (row 3, via compile), and there is no "instance plus new message" form (row 4). Row 5 matches exactly — a bare raise inside except re-raises the same object — and row 6 is a RuntimeError in both languages (the twin prints the type only; CPython's message, No active exception to reraise, is not keyed). Row 7 is the structural difference: a Python traceback is a chain of frame objects, not strings, so the replacement is exc.with_traceback(tb), and the twin shows the borrowed frames counted before and after. Row 8 has no fail. Row 9 agrees on the TypeError. Row 10 replaces $! and $@ with sys.exception() (3.11+) and sys.exc_info(), which likewise revert to None after the except block. Row 11 shows raise Klass is a plain constructor call — there is no exception hook to override.

Verified output of raise_has_four_forms_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. raise "msg": a string is not an exception
   TypeError
2. raise Klass instantiates it with no arguments
   ValueError: args=()
3. raise Klass, "msg" is Python 2 syntax
   SyntaxError
4. raise Klass("msg") is the one form; nothing replaces a message afterwards
   ValueError: args=('an instance',)
5. a bare raise inside except re-raises the same object
   outer got OSError; same object: True
6. a bare raise outside any except: a RuntimeError
   RuntimeError
7. with_traceback replaces the traceback (frames, not strings)
   frames in the borrowed traceback: 2; after re-raising with it: 3
8. no fail alias
   raise is the only spelling
9. raising something that is not an exception
   TypeError
   TypeError
   TypeError
10. sys.exception() inside except, and after it
   sys.exception() is e                    True
   sys.exc_info()[2] is e.__traceback__    True
   sys.exception() after the except        None
11. raise Klass calls Klass(): the constructor, not a hook
   Custom.__init__ called with ()
   Custom
   Custom.__init__ called with ('msg',)
   Custom

The raise statement is specified in the Python reference under The raise statement ↗.

Try it

  1. Write must_be_positive(n) that raises ArgumentError with the two-argument form and a message naming the offending value via inspect. Call it inside a begin whose rescue prints the class and message and then re-raises with a bare raise; catch that in an outer rescue and print whether the outer handler received the same object (equal?).
  2. Write check(value) that raises with caller as the third argument, call it from a method validate, and print e.backtrace.first[/in '(.*)'/, 1] to show the first entry blames validate. Then raise with caller(2) instead and see which entry comes first.
Solution to 1

raise_has_four_forms_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Exercise 1: raise with the two-argument form, rescue and report it, re-raise with a
# bare raise, and show that the outer handler receives the very same object.

def must_be_positive(n)
  raise ArgumentError, "expected a positive number, got #{n.inspect}" unless n.is_a?(Numeric) && n.positive?
  n
end

puts "1. a good value passes through"
puts "   must_be_positive(5) -> #{must_be_positive(5)}"

puts "2. a bad value is reported inside, then re-raised to an outer handler"
begin
  begin
    must_be_positive(-2)
  rescue ArgumentError => inner
    puts "   inner handler: #{inner.class}: #{inner.message}"
    raise
  end
rescue ArgumentError => outer
  puts "   outer handler got the same object: #{outer.equal?(inner)}"
end

puts "3. a non-number is reported with inspect, so a string shows its quotes"
begin
  must_be_positive("7")
rescue ArgumentError => e
  puts "   #{e.message}"
end

Verified output of raise_has_four_forms_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

1. a good value passes through
   must_be_positive(5) -> 5
2. a bad value is reported inside, then re-raised to an outer handler
   inner handler: ArgumentError: expected a positive number, got -2
   outer handler got the same object: true
3. a non-number is reported with inspect, so a string shows its quotes
   expected a positive number, got "7"

See also