West Wind .NET Tools and Demos
Preserving original exceptions in InvokeTaskMethodAsync
Gravatar is a globally recognized avatar based on your email address. Preserving original exceptions in InvokeTaskMethodAsync
  Manni Borg
  All
  Aug 28, 2026 @ 12:44am

Hi Rick,

I ran into an issue with InvokeTaskMethodAsync() when a Task fails with a custom exception.

For example, the .NET method throws an exception like this:

public class ApiException : Exception
{
    public int StatusCode { get; }
    public string ResponseBody { get; }

    public ApiException(string message, int statusCode, string responseBody)
        : base(message)
    {
        StatusCode = statusCode;
        ResponseBody = responseBody;
    }
}

In OnError():

FUNCTION OnError(cMessage, oException, cMethod)

I receive the correct message, but oException is a plain System.Exception, so the original exception type and properties such as StatusCode and ResponseBody are no longer available.

I saw that InvokeTaskMethodAsync() intentionally uses WrapException() to avoid COM visibility problems with custom exceptions, which explains the behavior.

Would it be possible to pass the original exception first and keep the current WrapException() behavior as a fallback if the custom exception cannot be passed to VFP?

Alternatively, would it be possible to add a bridge setting/property to enable or disable exception wrapping before calling a method? That would allow it to be disabled for calls where the developer knows the exception objects can safely be passed to VFP and needs access to the complete original exception. This would be very important for our development.

Either option would be great for getting access to the complete original exception.

Thanks a lot for taking a look!

Manni

EDIT:

After thinking about it a bit more, in case the property/setting approach is an option, to avoid problems with multiple async calls running in parallel and exceptions occurring later, the setting could perhaps be part of the callback class instead of loBridge itself, for example:

DEFINE CLASS MyCallback AS AsyncCallback
    lWrapTaskException = .F.
        ...

    FUNCTION OnError(cMessage, oException, cMethod)
        ...
    ENDFUNC
ENDDEFINE

InvokeTaskMethodAsync() could then read/snapshot this value at the beginning of the call and use that local value later when the Task completes. So the setting would always belong to the correct call without changing the existing method signature.

Gravatar is a globally recognized avatar based on your email address. re: Preserving original exceptions in InvokeTaskMethodAsync
  Rick Strahl
  Manni Borg
  Aug 28, 2026 @ 06:31am

The declared type is Exception but the actual exception instance should be the original object type.

If you use the indirect access methods on (ie. GetProperty() and InvokeMethod()) you should be able to access any extended properties of the Exception subclass. Direct access doesn't work because the type is Exception and it doesn't have the custom properties, but the properties are there and accessible via Reflection.

You can check by calling loBridge.GetTypeName(oException) which should show your ApiException (or whatever was thrown) and it should give you the type name of the instance rather than then declared type.

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: Preserving original exceptions in InvokeTaskMethodAsync
  Manni Borg
  Rick Strahl
  Aug 28, 2026 @ 01:48pm

Thanks Rick. I tested this again with a minimal example DLL so the behavior is completely isolated.

The test DLL is just this:

using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace AsyncExceptionTest
{
    [Guid("A76D5394-BA41-4D44-9A17-50B2A7A67A31")]
    [ClassInterface(ClassInterfaceType.AutoDual)]
    public class ExceptionTest
    {
        public async Task<string> HelloWorldAsync(string name)
        {
            await Task.Delay(100);

            throw new ApiException(
                "This is a test exception.",
                418,
                "Test response body");
        }
    }

    public class ApiException : Exception
    {
        public int StatusCode { get; }
        public string ResponseBody { get; }

        public ApiException(string message, int statusCode, string responseBody)
            : base(message)
        {
            StatusCode = statusCode;
            ResponseBody = responseBody;
        }
    }
}

So HelloWorldAsync() definitely throws an ApiException with StatusCode = 418 and ResponseBody = "Test response body".

In the VFP OnError() callback I checked the type using the bridge directly:

? loBridge.GetTypeName(oException)  &&  "System.Exception"

This returns: "System.Exception"

I then tried accessing the custom property through the bridge as you suggested:

? loBridge.GetProperty(oException, "StatusCode")

This fails because System.Exception.StatusCode can't be found.

I think the reason is shown in the comment and code of WrapException() in the current wwDotNetBridge.cs source itself, which is used in the exception handling path of InvokeTaskMethodAsync():

/// <summary>
/// Wraps an exception into a new Exception to avoid potential problems
/// with [ComVisible(false)] on .NET Core or with custom exceptions
///
/// This ensures that exceptions can be passed to FoxPro in Task scenarios
/// </summary>
private Exception WrapException(Exception ex, bool baseExcepection = true)
{
    if (baseExcepection)
        ex = ex.GetBaseException();

    // Create a new Exception in case there's a problem with [ComVisible]
    var ex2 = new Exception(ex.Message, ex.InnerException);
    return ex2;
}

So in the InvokeTaskMethodAsync() method the original exception doesn't appear to be passed through as an Exception reference to the original object. WrapException() actually creates a new plain System.Exception instance.

That would explain both results: GetTypeName() returns "System.Exception", and GetProperty() can't find StatusCode because that property no longer exists on the new exception instance.

So I think GetProperty() would work exactly as you described if the original custom exception instance were passed through, but currently WrapException() replaces it first.

That's why I suggested in my first post above an option to disable the WrapException() behavior for InvokeTaskMethodAsync() when access to the original exception and its custom properties is needed (e.g. with the option passed per call through the callback object).

Thanks, Manni

Gravatar is a globally recognized avatar based on your email address. re: Preserving original exceptions in InvokeTaskMethodAsync
  Rick Strahl
  Manni Borg
  Aug 29, 2026 @ 10:19am

LOL - I don't remember writing that code actually 😄 But I see why that might be necessary as many internal exceptions are explicitly marked as [ComVisible(false)].

In that case you might be able to get at it with the InnerException:

loBridge.GetProperty(oException,'InnerException.StatusCode')

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: Preserving original exceptions in InvokeTaskMethodAsync
  Manni Borg
  Rick Strahl
  Aug 29, 2026 @ 10:05pm

Thanks Rick. I think the problem is that the original exception isn't actually preserved as the InnerException in this case.

loBridge.GetProperty(oException, "InnerException")

returns

.NULL.

In my example, and also in a shared assembly we're working with that we can't simply modify, the custom exception itself has no InnerException:

throw new ApiException(
    "This is a test exception.",
    418,
    "Test response body");

And WrapException() currently does:

ex = ex.GetBaseException();
var ex2 = new Exception(ex.Message, ex.InnerException);

So in this example, after GetBaseException(), ex is still my ApiException, but ex.InnerException is .NULL. The resulting exception therefore has no InnerException, and the original ApiException (including StatusCode and ResponseBody) is lost.

Wouldn't WrapException() have to preserve ex itself as the InnerException for that approach to work, e.g.:

var ex2 = new Exception(ex.Message, ex);

I understand your point about some exceptions being [ComVisible(false)], though. That's why I was thinking about making the current wrapping behavior optional, so it remains the default for compatibility, but can be disabled when access to the original custom exception is needed.

Thanks, Manni

Gravatar is a globally recognized avatar based on your email address. re: Preserving original exceptions in InvokeTaskMethodAsync
  Rick Strahl
  Manni Borg
  Aug 30, 2026 @ 11:46am

Yes, looks like that should be passing in the ex not ex.InnerException...

I don't think we need to change any behavior (ie. turn off the wrap exception) if the innerException gives you the original exception.

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: Preserving original exceptions in InvokeTaskMethodAsync
  Manni Borg
  Rick Strahl
  Aug 30, 2026 @ 09:50pm

Yes, you're right, that's much cleaner. I went a bit overboard with my extra property 😄

It would be cool if you could include this change in one of the next updates!

Thanks, Manni

Gravatar is a globally recognized avatar based on your email address. re: Preserving original exceptions in InvokeTaskMethodAsync
  Rick Strahl
  Manni Borg
  Aug 31, 2026 @ 10:47am

It'll be fixed in the next update.

+++ Rick ---

© 1996-2026