Feature #21279
openBare "rescue" should not rescue NameError
Description
Abstract¶
Bare rescue
keywords (either as a modifier like foo rescue bar
or as clause of a begin
block) should not rescue NameError
or NoMethodError
.
This behaviour is unexpected and hides bugs.
Background¶
Many Rubyists are surprised to learn that NameError
is a subclass of StandardError
, so it's caught whenever you use a "bare" rescue
block.
begin
DoesNotExist
rescue => e
p e # => #<NameError: uninitialized constant DoesNotExist>
end
Similarly, NoMethodError
is also rescued, because it's a subclass of NameError
.
begin
does_not_exist()
rescue => e
p e # => #<NoMethodError: undefined method `does_not_exist' for main>
end
This is almost never expected behaviour. NameError
/NoMethodError
is usually the result of a typo in the Ruby source, that cannot be reasonably recovered from at runtime. It's a programming error just like a SyntaxError
, which isn't a StandandError
.
Proposal¶
No matter the solution, solving this problem will require a breaking change. Perhaps this could be part of Ruby 4?
The most obvious solution is to change the superclass of NameError
from StandardError
to Exception
(or perhaps ScriptError
, similar to SyntaxError
).
Alternatives considered¶
If we want to avoid changing the inheritance hierarchy of standard library classes, we could instead change the semantics of bare rescue
from "rescues any subtype of StandardError
", to instead be "rescues any subtype of StandardError
except NameError
or its subtypes". This is worse in my opinion, as it complicates the semantics for no good reason.
Use cases¶
fun example
The worst case I've seen of this came from a unit tesat like so:test "aborts if create_user returns error" do
mock_user_action(data: {
user: { id: 123, ... },
errors: [{ code: "foo123" }]
})
ex = assert_raises(StandardError) do
CreateUser.perform(123)
end
assert_match(/foo123/, ex.message)
end
This test passes, but not for the expected reason. It turns out that inside of the business logic of CreateUser
, the error code data was accessed as a method call like error.code
, rather than a key like error[:code]
. This lead to:
NoMethodError (undefined method `code' for {:code=>"foo123"}:Hash)
The NoMethodError
is a StandardError
, and even more insidious, because foo123
is part of the NoMethodError's default message, the assert_match(/foo123/, ex.message)
also mathches!
The correct fix here would be to introduce a specific error like UserCreationError
that can be rescued specifically, with a field like code
that can be matched instead of the message. Regardless, this illustrates the kind of confusion that comes from NoMethodError
being a StandardError
.
Discussion¶
It might be useful to distinguish between NameError
s made in "static" code like DoesNotExist
or does_not_exist()
, versus those encountered dynamically via Object.const_get(dynamic_value)
or object.send(dynamic_value)
. In those metaprogramming cases, the error could be a consequence of bad runtime data, which is more recoverable than just some fundamental error with your Ruby code.
No data to display