
Issue #21279 has been updated by byroot (Jean Boussier). Maybe I'm simply used to how this worked until now, but I don't see a problem with `NoMethodError` being handled by bare `rescue`. 99% of the time, when I use a bare `rescue` it's because I'll call `raise` again, or calling unknown code and I need to not crash, etc. So to me it makes sense to rescue about everything except "system" issues like `NoMemoryError` etc.
merge to master and see if it actually breaks things, and how badly
I can already tell this is going to break a lot of things. Worse I'm pretty sure it's going to break things in production, but not be caught in test because these codepaths aren't necessarily tested for `NoMethodError` specifically. So I'm personally very opposed to this change. ---------------------------------------- Feature #21279: Bare "rescue" should not rescue NameError https://bugs.ruby-lang.org/issues/21279#change-112823 * 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/