Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RuntimeWrappedException cannot be caught #18374

Open
IS4Code opened this issue Mar 11, 2025 · 4 comments
Open

RuntimeWrappedException cannot be caught #18374

IS4Code opened this issue Mar 11, 2025 · 4 comments
Labels
Area-Compiler-CodeGen IlxGen, ilwrite and things at the backend Bug Impact-Low (Internal MS Team use only) Describes an issue with limited impact on existing code.
Milestone

Comments

@IS4Code
Copy link

IS4Code commented Mar 11, 2025

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
Copy link
Author

IS4Code commented Mar 11, 2025

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.

@T-Gro
Copy link
Member

T-Gro commented Mar 14, 2025

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
Copy link
Member

T-Gro commented Mar 14, 2025

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.

@IS4Code
Copy link
Author

IS4Code commented Mar 14, 2025

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

@abonie abonie added Impact-Low (Internal MS Team use only) Describes an issue with limited impact on existing code. Area-Compiler-CodeGen IlxGen, ilwrite and things at the backend and removed Needs-Triage labels Mar 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compiler-CodeGen IlxGen, ilwrite and things at the backend Bug Impact-Low (Internal MS Team use only) Describes an issue with limited impact on existing code.
Projects
Status: New
Development

No branches or pull requests

3 participants