`RuntimeWrappedException` cannot be caught
When a non-Exception object is thrown (from CIL or other languages that support it), it cannot be caught in F# by default.
Repro steps
let throwobj (x:obj) = (# "throw" x #) // or any other code that throws a non-Exception instance
try
throwobj "test"
with
| e -> printf "%O" e
Expected behavior
The exception should be caught either as System.String or System.Runtime.CompilerServices.RuntimeWrappedException.
Actual behavior
Unhandled exception. System.InvalidCastException: Unable to cast object of type 'System.String' to type 'System.Exception'.
This is caused by the generated exception handler:
.try
{
ldstr "test"
throw
// ...
}
catch [netstandard]System.Object
{
castclass [System.Runtime]System.Exception // InvalidCastException
stloc.0
// ...
}
F# here catches everything, but the cast to Exception throws another exception since the object is not an Exception.
Known workarounds
The code above succeeds if the attribute that controls how wrapped exceptions are exposed is used explicitly:
[<assembly: System.Runtime.CompilerServices.RuntimeCompatibility(WrapNonExceptionThrows = true)>]do()
Related information
Throwing a RuntimeWrappedException manually does not exhibit this behaviour because the runtime does not treat it as a special case then and does not unwrap the value.
Environment: https://sharplab.io/#v2:DYLgZgzgNALiCWwA+wCmMAEMAWAnA9gO74BGAVhgBQAeIpZAlBgLxUDEGARDgYZxtQxsGAWABQ4mLgCe4jFjxF6XGKggxO4wvBzikGVBgC0APgwAHXPAB2MMFwCkAeX6ogA=
There are two options how I see this being addressed:
- Apply
RuntimeCompatibilityAttributeimplicitly. This makes it behave exactly like C# in this situation. - Change
castclasstoisinst. It is however unclear how the exception should be exposed/caught in that case, since the language applies the catch patterns on theExceptiontype, notobj.
I think the first option is the safest one. It is a potential breaking change if people rely on try-with throwing an InvalidCastException in this case, but I don't think that is the supposed behaviour.
To assess the impact, is there a library/framework/other_compiler making use of non-Exception throws practically?
I would not change the codegen, but RuntimeCompatibility(WrapNonExceptionThrows = true) could be considered if this happens in real world.
Another way of addressing this would be a compiler-intrinsic active pattern binding a plain obj ;; and the compiler would be aware of it and not emit the castclass.
Application code would then need to use this specific active pattern to catch non-Exception throws.
@T-Gro I am not aware of a concrete case where this would happen. I can see interop with C++/CLI running into the possibility, but non-CLS exceptions are seldom used in other situations. It is just something I like to check out of curiosity. 😉 Also both C# and VB.NET emit RuntimeCompatibility by default so I feel it would make sense for F# to align with for consistency since it also cannot handle non-CLS exceptions (honestly I don't see why WrapNonExceptionThrows = true was not made the default in CLR anyway).
I like the idea of a compiler-intrinsic active pattern though, but it could work with WrapNonExceptionThrows = true too:
let (|RuntimeWrappedException|_|)(x : Exception) =
match x with
| :? RuntimeWrappedException as wrapped -> Some (wrapped.WrappedException)
| _ -> None
There is a slight difference in doing it that way compared to the intrinsic due to the fact that you can actually construct a RuntimeWrappedException manually from a value, and if you throw and catch that, the runtime will not unwrap it even if you don't have WrapNonExceptionThrows = true, so you can, in some way, distinguish "true" wrapped exceptions from constructed ones. I doubt that is of any practical use though.