fsharp icon indicating copy to clipboard operation
fsharp copied to clipboard

`RuntimeWrappedException` cannot be caught

Open IS4Code opened this issue 10 months ago • 4 comments

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=

IS4Code avatar Mar 11 '25 12:03 IS4Code

There are two options how I see this being addressed:

  • Apply RuntimeCompatibilityAttribute implicitly. This makes it behave exactly like C# in this situation.
  • Change castclass to isinst. It is however unclear how the exception should be exposed/caught in that case, since the language applies the catch patterns on the Exception type, not obj.

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.

IS4Code avatar Mar 11 '25 12:03 IS4Code

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.

T-Gro avatar Mar 14 '25 13:03 T-Gro

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 avatar Mar 14 '25 13:03 T-Gro

@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.

IS4Code avatar Mar 14 '25 14:03 IS4Code