Error handling

In this chapter the handling of errors in automation servers and in automation clients is discussed. 

An exception occurs

In every piece of software things (can) go wrong. This can be due to anything from bad coding to hardware failure. Automation servers are no exception to this. Whenever an error occurs in a piece of Delphi code an exception is raised. Exceptions are the windows standard of dealing with errors, they provide a place for at least an error-message and an error-code.

Let's take a very simple server DoesBang, with a buggy method Boom

procedure TDoesBang.Boom;
   ShowMessage(Format('%s', [123456]));

This server will be used by a VBA client. VBA is very well capable of catching exceptions using On error. The goto statements are a horror to a true Delphi developer, but the code does catch the exception and show its information. 

Dim Bang As New DoesBang

Sub DoBang()
   On Error GoTo CatchError
   MsgBox ("Made it")
   Exit Sub
   MsgBox (Err.Description & " The hex errorcode is : " & Hex(Err.Number))
   GoTo NormalExit
End Sub

When the dobang macro is run Word pops up a dialog:

This message looks very familiar. In the method I passed a numeric value to the format procedure and the string to be formatted needed a string value. This raises an exception and the message is format's complaint. The exception raised in the Delphi code seems to be caught and understood by VBA.

Behind the scenes

At a first glance its seems if an exception is raised in the automation server and is handled in Word. But according to the COM specifications an automation server has to handle all exceptions itself, not a single one should leak through. To understand what is going on we will have to take a deeper look at the code in the server.

The buggy method is declared as a safecall:

procedure Boom; safecall;

Delphi Exceptions occurring inside a safecall are handled different. Control will be passed to tObject.SafeCallexception. The implementation can be found in the VCL unit system.pas

function TObject.SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult;
   Result := HResult($8000FFFF); { E_UNEXPECTED }

This result matches the error code displayed by VBA. It is cast to an hResult, this hResult can be found in the declaration of Boom in the typelibrary

HRESULT _stdcall Boom( void );

Every method of an automation server is actually a function with an hResult result type. In the first chapter I showed how the "functional" function result is a part of the parameter list.  If all goes well in the method an hResult of S_OK ($0) is returned by the function. And in the case of an exception E_UNEXPECTED ($8000FFFF) is returned thanks to tObject.SafeCallexception.

An hResult is a handle to a result. It is a 32 bit value in which the first bit indicates success (a value of 0) or failure ( a value of 1). $8000FFFF forms these 32 bits : 10000000000000001111111111111111. The first bit is a 1 so VBA knows the call to boom was a failure and will call VBA's error handling system. Follow 15 zero bits, this is FACILITY_NULL. Closing is the error number FFFF, the final error holding the nice message "catastrophic failure".

Returning hResult error codes

Delphi translates any exceptions in my server's code to an automation compatible format. The useful information the client received was a string accompanied by a very general hResult, E_UNEXPECTED.

Some error conditions are very well recognized by the server. In which case the server would like to raise an exception to pass this info to the client. I can use an hResult again. The Delphi VCL has an automation exception type ready, EoleSysError. This exception class has a constructor which takes an hResult.

An hResult has a length of 32 bits and consist of three parts. The first bit indicates the success, being SEVERITY_ERROR or SEVERITY_SUCCESS, both are declared in windows.pas. The next 15 bits are the facility again, they indicate the group the error belongs to. All errors in custom automation servers should use FACILITY_ITF, also declared in windows.pas. The third part is the error-number itself, which should have a value between $0200 and $FFFF. When raising an error I will construct my own hResult 

const MyError1 = 1;
raise EoleSysError.Create('My personal error',
        (SEVERITY_ERROR shl 31) or (FACILITY_ITF shl 16) or ($0200 + MyError),

The last parameter is the helpcontext. The helpfile is a property of the automation server and can be defined in the typelibrary or can be set at runtime by setting the helpfilename property of the global  ComServer object.

When VBA executes a method in my server, and the server hits this error the messagebox pops up again :

VBA understands the exception quite well and will displays it's info.

Catching errors in the automation server

We have seen how VBA catches the exceptions and works with the hResult. An automation client written in Delphi uses the same technique. When a call to method leads to a SEVERITY_ERROR hresult Delphi will translate this in raising an exception of type EoleException.

This piece of code from a Delphi project named bugclient calls the buggy method:

   fBang:= CoDoesBang.Create;

It  will result in a well known Delphi exception dialog :

My server returned an hresult which is is marked as SEVERITY _ERROR. As a reaction to this value The COM api on the client will raise an EoleException. This exception can be caught in a try except block. This can be used to retrieve the specific hResult 

   on E : EoleException do
         ShowMessage(Format ('%s The Hex errorCode is %x',[E.Message, E.ErrorCode ]));

The exception is caught and the hResult I constructed in the server can be found in the errorcode property of EoleException

Now I am catching all OLE Exceptions in the client. The setting of an hResult In tObject.SafeCallException has the same result as explicitly raising an EoleException in a server method. All end here. The difference between these exceptions as the client sees them is the Factility value, a catastrophic failure has FACILITY_NULL and in my EoleSysException I was given FACILITY_ITF.  If I am lucky the server has provided some info in the Errorcode or Message, if I am in bad luck there is only $800FFFF there.

Catching errors when programming the COM API

hResult's are everywhere in the automation API. All helper functions return an hResult. Whatever terrible things happen in there, you will miss it if you don't check the function's result. To avoid having to code 10 level deep if statements the VCL came up with the OLEcheck helper function.

procedure OleCheck(Result: HResult);
   if not Succeeded(Result) then OleError(Result);

The result of the API function is passed in the param. It is checked for the error bit. If this is set the OleError helper function will be called, passing the hResult.

procedure OleError(ErrorCode: HResult);
  raise EOleSysError.Create('', ErrorCode, 0);

All this helper does is raise an OLE exception, passing its constructor the hResult. Using OleCheck you can program like this :

   OleCheck(CreateBindCtx(0, BindCtx));
   OleCheck(GetRunningObjectTable(0, ROT));
   OleCheck(CreateClassMoniker(CLASS_SharedZapper, IMonik));
   OleCheck(ROT.Register(0, Imonik, Imonik, cookie));

I am trying to make 4 calls on the COM API. What these calls do is part of a later chapter, for the moment it is enough to know that each of them can go wrong. This will be noticed by the OLEcheck helper which will raise an exception. By re-raising the exception in the except/end block I will be confronted with the error message of the failing API call.

Where are we ?

Success or failure of an automation call is described in the hResult code. When an exception occurs there is a lot going on behind the scenes. The Delphi runtime system takes very good care of exceptions leaving and exceptions entering your code, making the handling of exceptions across COM borders fully transparent. Which means you do not have to take explicit care of exceptions. 

To pass specific error information to clients a server should raise an EoleSysError with a custom hResult. Incoming exceptions are of type EoleException and can be inspected as such to obtain the hResult.

What's next ?