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