Project

General

Profile

Actions

Feature #21279

open

Bare "rescue" should not rescue NameError

Added by AMomchilov (Alexander Momchilov) 5 days ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:121700]

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 NameErrors 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

Actions

Also available in: Atom PDF

Like0