[ruby-core:121737] [Ruby Feature#21279] Bare "rescue" should not rescue NameError
From:
"Eregon (Benoit Daloze) via ruby-core" <ruby-core@...>
Date:
2025-04-26 18:18:26 UTC
List:
ruby-core #121737
Issue #21279 has been updated by Eregon (Benoit Daloze).
I think the proposal makes sense and would be worth to try it experimentally (i.e. merge to master and see if it actually breaks things, and how badly), if matz agrees to it.
I recall being bitten by this a couple times as well, it's all too simple to accidentally catch typos in method names as StandardError with bare `rescue`.
----------------------------------------
Feature #21279: Bare "rescue" should not rescue NameError
https://bugs.ruby-lang.org/issues/21279#change-112792
* Author: AMomchilov (Alexander Momchilov)
* Status: Open
----------------------------------------
# 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`](https://docs.ruby-lang.org/en/master/NameError.html) is a subclass of [`StandardError`](https://docs.ruby-lang.org/en/master/StandardError.html), so it's caught whenever you use a "bare" `rescue` block.
```ruby
begin
DoesNotExist
rescue => e
p e # => #<NameError: uninitialized constant DoesNotExist>
end
```
Similarly, [`NoMethodError`](https://docs.ruby-lang.org/en/master/NoMethodError.html) is also rescued, because it's a subclass of `NameError`.
```ruby
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`](https://docs.ruby-lang.org/en/master/SyntaxError.html), 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`](https://docs.ruby-lang.org/en/master/ScriptError.html), 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
<details><summary>fun example</summary>
The worst case I've seen of this came from a unit tesat like so:
```ruby
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`.
</details>
# 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.
--
https://bugs.ruby-lang.org/
______________________________________________
ruby-core mailing list -- ruby-core@ml.ruby-lang.org
To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org
ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/