Quiz: Gotcha with Exceptions and HRESULTs


The C# code below, when executed, prints the following:


  0x80004002


  0x80004002


Who can figure out why the second line printed isn’t 0x80004003?


using System;


using System.Runtime.InteropServices;


 


public class Quiz


{


  const int E_NOINTERFACE = unchecked((int)0x80004002);


  const int E_POINTER = unchecked((int)0x80004003);


 


  public static void Main ()


  {


    try


    {


      Marshal.ThrowExceptionForHR(E_NOINTERFACE);


    }


    catch (Exception ex)


    {


      Console.WriteLine(“0x{0:x}”, Marshal.GetHRForException(ex));


    }


 


    try


    {


      Marshal.ThrowExceptionForHR(E_POINTER);


    }


    catch (Exception ex)


    {


      Console.WriteLine(“0x{0:x}”, Marshal.GetHRForException(ex));


    }


  }


}

Comments (12)

  1. Andy Hopper says:

    Well, both E_NOINTERFACE and E_POINTER HRESULT are mapped to an InvalidCast exception by ThrowExceptionForHR. Once an InvalidCast exception is thrown, the same HRESULT will be returned from GetHRForException (they’re the same exception at that point).

  2. Phil says:

    The reason that 0x8004002 is displayed both times is that the error object is not being reset between the two try/catch blocks. before the first try/catch the error object associated with the logical thread is empty, after the try/catch the error object’s HRESULT value is set to 0x8004002. Now the second try/catch block runs and the GetErrorInfo() is run to set the COMException.HRESULT value to the value from the error object (which is already set to 0x8004002), so the COMException.HRESULT value is set to 0x8004002. Let me see if I can come up with an example…

  3. Phil says:

    Well, I was able to demonstrate why this is happening (see the code below). I inserted a call to create a new instance of the Excel.ApplicationClass on the Excel COM Server. Any cross-apartment call that utilizes a proxy-stub or any COM call within an apartment that uses the COM message loop (STA) will clear the error info object. So, the call to the Excel COM object between the caught exceptions will clear the error info object:

    class Class1
    {
    const int E_NOINTERFACE = unchecked((int)0x80004002);
    const int E_POINTER = unchecked((int)0x80004003);
    private Excel.Application _ExcelApplication = null;

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            TryInterface();
            CallExcel();
            TryPointer();
    
            int i = 0;
        }
    
        static void CallExcel()
        {
            Excel.ApplicationClass ac = new Excel.ApplicationClass();
            string s = ac.DefaultFilePath;
            Console.WriteLine(s);
        }
    
        static void TryInterface()
        {
            try
            { 
                Marshal.ThrowExceptionForHR(E_POINTER); 
            } 
            catch (Exception ex) 
            { 
                Console.WriteLine(ex.ToString());
                Console.WriteLine("0x{0:x}", Marshal.GetHRForException(ex)); 
            } 
        }
    
        static void TryPointer()
        {
            try 
            { 
                Marshal.ThrowExceptionForHR(E_NOINTERFACE); 
            } 
            catch (Exception ex) 
            { 
                Console.WriteLine(ex.ToString());
                Console.WriteLine("0x{0:x}", Marshal.GetHRForException(ex)); 
            }
        }
    }
    
  4. Andy Hopper says:

    Yep. Egg is on my face. A simple test proved that I was wrong.

  5. Phil says:

    Interesting. In my test, I also tried Marshal.GetLastWin32Error after the TryInterface(), CallExcel() and TryPointer() functions but it returned 0. I also read somewhere that managed code should never call GetLastError() directly since the results from the GetLastError() call may not be accurate because the CLR may have made additional calls that would modify the logical thread error object.

    Adam, could you give some background on how you found this bug/feature?
    Also, did my example demonstrate what’s going on, or is there another reason why this is happening?

  6. Fernando Tubio says:

    I don’t have much else to add but one thing I noticed is that in both cases the code is throwing the same instance of the exception. Also, if you throw each exception in it’s own thread, then it prints the expected result.

  7. Adam Nathan says:

    Very good observations, everyone!

    Ordinarily, E_NOINTERFACE is mapped to InvalidCastException and E_POINTER is mapped to NullReferenceException. But as Fernando pointed out, both exceptions thrown in this example are InvalidCastException. That explains why GetHRForException returns E_NOINTERFACE in both cases, but why do we get an InvalidCastException twice?

    The ThrowExceptionForHR method throws an exception whose type corresponds to the input HRESULT value (or a COMException containing the HRESULT if the input HRESULT isn’t in the CLR’s mapping table). But it does more than just that: it calls the Win32 GetErrorInfo API to attempt to retrieve an error object available on the current thread. It uses this information to give the exception a customized error message, source, and so on. ThrowExceptionForHR also has an overload with a second System.IntPtr parameter that represents a pointer to an IErrorInfo interface, so you can pass this information directly. Passing IntPtr.Zero means that you want the API to act just like the single-parameter overload (and call GetErrorInfo), and passing -1 disables the extra info retrieval altogether.

    The GetHRForException method retrieves the HRESULT value for any managed exception. This is the value of the exception’s protected "HResult" property, which you usually can’t access directly. But this method has a side effect: it calls the Win32 SetErrorInfo API, passing the CCW for exception (which implements IErrorInfo appropriately). It does this to make it easy to mimic the marshaler’s behavior when manually exposing an exception as an HRESULT and IErrorInfo to COM.

    In this example, the first call to GetHRForException causes the CLR to call SetErrorInfo with the wrapped InvalidCastException object. Because of this, the second call to ThrowExceptionForHR picks up this information when it calls GetErrorInfo, as Phil pointed out. The reason this affects the type of the thrown exception – despite the fact that IErrorInfo doesn’t have a mechanism for retrieving an HRESULT – is that the CLR can detect when the error object retrieved from SetErrorInfo originated as a managed exception and just reuse that original exception. All of this means that no matter what HRESULT is passed into the ThrowExceptionForHR method the second time (as long as it’s a failure HRESULT), the thrown exception is always an InvalidCastException.

    To get the behavior you’d ordinarily expect, you can use the overload of ThrowExceptionForHR that ignores IErrorInfo:

    using System;
    using System.Runtime.InteropServices;

    public class Quiz
    {
    const int E_NOINTERFACE = unchecked((int)0x80004002);
    const int E_POINTER = unchecked((int)0x80004003);

    public static void Main()
    {
    try
    {
    Marshal.ThrowExceptionForHR(E_NOINTERFACE);
    }
    catch (Exception ex)
    {
    Console.WriteLine("0x{0:x}", Marshal.GetHRForException(ex));
    }

    try
    {
      // Ignore IErrorInfo
      Marshal.ThrowExceptionForHR(E_POINTER, new IntPtr(-1));
    }
    catch (Exception ex)
    {
      Console.WriteLine("0x{0:x}", Marshal.GetHRForException(ex));
    }
    

    }
    }

    This new code prints:

    0x80004002
    0x80004003

    Phil asked why Marshal.GetLastWin32Error always returned 0 no matter where he inserted it. That’s because GetLastError/SetLastError is a separate mechanism from GetErrorInfo/SetErrorInfo. The former mechanism is never involved here. And, yes, managed code should never call GetLastError directly. See: http://blogs.gotdotnet.com/anathan/PermaLink.aspx/6d021f27-72c8-4420-b6ad-1c5f0690bde8

  8. mgkind says:

    I am sorry that my guestion is not related to the subject.
    I would like to know that how can I gather information of one page for example get the html code of one special page in my ASP or ASP.NET apllication and use it in my application.for exaple I get the html code of "www.anysite.com " and then use this code in my page by programming.

    thanks

  9. Sunny Gulati says:

    Nathan, hoping that you get email-notified/etc/whatnot… This stuff is totally not in my comfort zone..

    Is there a more standard way than "invoke Excel" to clear the errorcode? I think i might have a situation where one failed interop call is causing another one to think it has bombed (when it actually has not). (mixing AD and Sharepoint API’s)

    if AD does not throw an exception -> sharepoint is totally happy.

    If AD does throw an exception (which we catch, btw) -> sharepoint bombs before rending the page.

    -S.

  10. Euphemos says:

    Getting information on a COM failure