crystal-lang / crystal

The Crystal Programming Language

Home Page:https://crystal-lang.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Allow rescuing exceptions based on included modules

Blacksmoke16 opened this issue · comments

Feature Request

Currently exceptions may only be rescued via their explicit type. I propose to expand this to modules that an exception includes. For example:

module MyApp::Exception; end

class MyApp::Exception::SomeError < ArgumentError
  include MyApp::Exception
end

begin
  # ...
rescue ex : MyApp::Exception::SomeError # Rescue explicitly `SomeError` within `MyApp`
end

begin
  # ...
rescue ex : MyApp::Exception # Rescue all exceptions within `MyApp`
end

begin 
  # ...
rescue ex : ArgumentError # Rescue all forms of `ArgumentError`
end

My use case for this is each Athena component defines some exception types that inherit from applicable stdlib Exception types. However, I would also like to be able to define some Exception module within each component to include in all of its custom exception types. I think this makes sense since they are all logically related given they're defined in the same namespace of a singular project.

At a high level it just makes sense being able to rescue a specific component specific exception, a more generic stdlib exception type, all exceptions, or all exceptions from a specific shard.

A more concrete example of how this could be useful is say some exceptions may be raised signifying validation/improper argument usage. Internally within the component there are a few cases where I'd like to be able to rescue these internal exceptions to ignore them while still allowing stdlib internal exceptions (e.g. ArgumentError) to bubble up. Currently you'd have to use a union and remember to update all the usages of said union.

Another way would be to have an abstract class inherit from ::Exception, and use that as the base type. The problem with that is it removes the ability to also inherit from the stdlib base type. E.g. having a more specific ArgumentError.

I did some research into this and it seems like it's possibly in Ruby, PHP, and maybe otheres?

First mentioned in #11639 (comment).

EDIT: This would also allow rescuing any SystemError

This seems very reasonable and I wouldn't have been surprised if it already worked this way. It's even a bit unexpected that it doesn't. Esepcially since it's supported in Ruby. So might just be an unintentional omission.

In my mental model, rescue ex : Type is equivalent to an elective type restriction with re-raising on non-match.

rescue ex : Foo
  bar
end

This is more convenient to write than the long form:

rescue ex
  if ex.is_a?(Foo)
    bar
  else
    raise ex
  end
end

The actual type casting seems to work fine with module types. It's just the extra validation that checks for being a descendent of ::Exception which makes it impossible to filter for modules.
So I think the implementation could be pretty trivial:

--- i/src/compiler/crystal/semantic/main_visitor.cr
+++ w/src/compiler/crystal/semantic/main_visitor.cr
@@ -2775,7 +2775,7 @@ module Crystal
         types = node_types.map do |type|
           type.accept self
           instance_type = type.type.instance_type
-          unless instance_type.implements?(@program.exception)
+          unless instance_type.implements?(@program.exception) || instance_type.module?
             type.raise "#{instance_type} is not a subclass of Exception"
           end
           instance_type