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¶
- Write
must_be_positive(n)that raisesArgumentErrorwith the two-argument form and a message naming the offending value viainspect. Call it inside abeginwhoserescueprints the class and message and then re-raises with a bareraise; catch that in an outerrescueand print whether the outer handler received the same object (equal?). - Write
check(value)that raises withcalleras the third argument, call it from a methodvalidate, and printe.backtrace.first[/in '(.*)'/, 1]to show the first entry blamesvalidate. Then raise withcaller(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¶
- Custom exceptions subclass
StandardError— the instance-sideexceptionhook that letsraise objraise a non-exception object - Exceptions have a
cause— what araiseinside arescuerecords about the exception it replaces rescue,elseandensurerun in a fixed order — the other end of every row here- A bare
rescuecatchesStandardError, not everything — why theRuntimeErrorof row 1 is rescued by default and aSystemExitis not - Backtraces and
caller— the third argument of row 7 in more depth - Kernel#raise ↗ — the reference for every form here, including
cause: - Common error messages — Twenty-nine errors you will meet in your first month of Ruby, each with the code that raises it, the…