Raising Errors from a Server

[Previous] [Next]

There will undoubtedly be times when a client will call upon your component to perform a task but your code can't fulfill the request. This might happen if the database server has gone off line, for example, or if the caller doesn't have the proper authorization. Perhaps the client has attempted to run a transaction that violates one of your business rules. As a component author, you must anticipate these problems. In these situations, you must raise errors and propagate them from your code back to the caller.

If you're a long-time Visual Basic programmer, you're probably comfortable with Visual Basic's native support for raising and handling errors. When you want to propagate an error back to the caller, you simply use the Raise method of the built-in Err object. On the client side, you handle errors by using On Error statements and creating error handlers. However, COM is all about integrating code from different languages. When an error propagates across component boundaries, it must do so in a language-neutral fashion.

Let's say you created a component with Visual Basic that raises an error. When other Visual Basic programmers access your component, they'll handle your error in the same fashion as any other runtime error. However, other client-side programmers might access your component using different languages, such as C++, VBScript, and JavaScript. Fortunately, COM provides a way for these types of clients to handle your errors as well.

COM provides two language-independent mechanisms for reporting error conditions. A COM object uses both HRESULTs and COM exceptions to inform the client when a call can't be completed successfully. The HRESULT is the standard return value that ultimately tells the client whether a call was successful. COM exceptions are layered on top of HRESULTs and are used to propagate rich error information from an object back to a client in a language-neutral fashion.

Visual Basic had its own native support for raising and handling errors before the COM team created their own exception-handling model. The Visual Basic and COM team worked hard so that Visual Basic's error-handling model could be directly mapped on top of COM exceptions. The Visual Basic runtime deals with the HRESULTs and COM exceptions behind the scenes. You simply raise errors from your components and handle errors in your client code. Visual Basic does the automatic conversion for you behind the scenes. This is great news for Visual Basic programmers—you don't have to change your style of programming when you start creating components.

HRESULTs

HRESULTs indicate the success or failure of method calls in COM. They're particularly important in calls that extend across the network. COM has standardized on this return value so that the remoting infrastructure (the RPC layer) can inform the client that a problem has occurred when an object can't be reached. Therefore, if an object that's part of an active connection meets an untimely death, the client can discover this fact and respond appropriately.

In COM, every remotable method must return an HRESULT. To be remotable, a method must be callable across a proxy/stub layer. As it turns out, every method you ever create or call using Visual Basic returns an HRESULT. However, the Visual Basic team has tried its best to hide all traces of the existence of HRESULTs. If you use OLEVIEW.EXE to inspect your servers, you'll see that every method you've created is defined with an HRESULT as a return value—including Visual Basic methods that are defined as functions. For example, a method defined in Visual Basic as

 Function GetTaxRate() As Double 

is expressed in IDL like this:

 HRESULT GetTaxRate([out, retval] double*); 

Whenever a method needs a logical return value, IDL accommodates this requirement by providing the [retval] parameter attribute. There can be only one [retval] parameter in a COM method, and it must appear last in the parameter list. Visual Basic and Java are examples of languages that transparently map these special parameters so that programmers can use them as return values instead of as output parameters. Mapping makes calling a function more natural and also relieves programmers from manually inspecting HRESULTs. Visual Basic's runtime layer inspects the HRESULT, and the client receives an error in the event of a failure.

Why does COM need a standard return value? A standard method return value allows COM's infrastructure to return well-known errors back to the client in certain situations. To demonstrate the value of standard return values, let's suppose that a client is connected to a remote object that lives across the network. There's a chance that the object will die or become unreachable while the client still holds a connection—for example, if the server process crashes or if an overzealous administrator reboots the server computer. In either case, the client application will have an outstanding reference to an object that no longer exists. This situation can also occur if network problems make it impossible to route a method request between the client and the object. In all such cases, a remote method call will fail.

The underlying RPC layer is capable of informing the client that the server is unavailable. This is one of the primary reasons that the COM team chose to standardize on HRESULTs. A client can inspect an HRESULT to see whether the call was successful. If the client determines that the call was not successful, it can also examine the HRESULT to find out more specific information about the problem at hand. The HRESULT tells the client whether the error came from the object itself or from another subsystem such as the underlying RPC layer.

As it turns out, only C++ programmers deal with HRESULTs directly. Visual Basic makes things much easier. On the client side, the Visual Basic runtime always inspects HRESULTs for you behind the scenes. If a call returns without an error, it means that the Visual Basic runtime determined that the HRESULT indicated success. But if the Visual Basic runtime sees an HRESULT that indicates failure, it raises a runtime error.

Do you ever have to deal with HRESULTs as a Visual Basic programmer? Only in rare situations. The Visual Basic runtime knows about many well-known HRESULTs that are reported by the COM library as well as by other various COM-related subsystems. The Visual Basic runtime translates these HRESULTs into standard runtime errors. However, the Visual Basic runtime doesn't handle certain other situations as elegantly. If it encounters an HRESULT that it doesn't know about, it raises an error and places the raw HRESULT value in Err.Number.

Visual Basic 6 added support for catching most of the HRESULTs that indicate that an out-of-process object is dead or unreachable. When the Visual Basic runtime sees HRESULTs from the RPC layer that report a dead object, it raises error number 462 along with this description: "The remote server machine doesn't exist or is unavailable." This runtime error assists you in many of the cases in which you need to handle a dead or unreachable object. However, Visual Basic 5 doesn't include the same support. If you're using Visual Basic 5, you must examine a raw HRESULT to determine that an object has died. At times like these, it's helpful to know exactly what's inside an HRESULT.

An HRESULT is a 32-bit value that contains three distinct pieces of information. Figure 4-4 shows how the physical memory of an HRESULT is segmented to hold a severity level, a facility code, and an application-specific error code. The severity level stored in bit 31 indicates whether the method call was successful. If the bit is left off, the method call succeeded. If the bit is turned on, the method call failed.

click to view at full size.

Figure 4-4 All remotable methods in COM return HRESULTs at the physical level.

The set of bits in the middle of an HRESULT, known as the facility code, tells the client where the error originated. Standard COM errors use the code FACILITY_NULL. An object, on the other hand, should use FACILITY_ITF when it wants to send an interface-specific error code to the client. Table 4-2 shows other possible values for the facility code. As you can see, errors can be generated from many different subsystems in Windows 2000.

How do you know when you're dealing with a raw HRESULT in Err.Number? It's actually pretty easy to tell because the value is a very large negative number. When you see a value such as -2147220992, you know it's an HRESULT indicating failure. The most significant bit of an HRESULT tells the caller whether the call was successful. If this bit is turned on, the call failed. However, Visual Basic always sees this bit as an indicator of whether the integer is positive or negative because Visual Basic always uses signed integers rather than unsigned integers.

If you experience a runtime error that passes you a raw HRESULT, you can attempt to find out what happened by inspecting both the facility code and the error code. However, to extract the facility code or the error code, you must perform a bitwise operation using the AND operator. Look at the following function:

 Function GetFacilityCode(ByVal HRESULT As Long) As Integer     GetFacilityCode = ((HRESULT And &HFFF0000) / 65536) End Function 

This function extracts the bits of the HRESULT that are specific to the facility code. It uses a bit mask to ignore all the other bits. It then divides by 65536 to calculate the actual facility code. Table 4-2 shows all the facility code values that are defined in Windows 2000.

Table 4-2 Facility Codes in Windows 2000

Facility CodeValue
FACILITY_NULL 0
FACILITY_RPC 1
FACILITY_DISPATCH 2
FACILITY_STORAGE 3
FACILITY_ITF 4
FACILITY_WIN32 7
FACILITY_WINDOWS 8
FACILITY_SECURITY 9
FACILITY_CONTROL 10
FACILITY_INTERNET 12
FACILITY_MEDIASERVER 13
FACILITY_MSMQ 14
FACILITY_SETUPAPI 15
FACILITY_SCARD 16

By examining the facility code, you can try to determine where the error occurred. Facility codes such as FACILITY_RPC, FACILITY_WIN32, and FACILITY_WINDOWS mean that the error was reported by the underlying infrastructure. These codes might also mean you have a dead or unreachable object. Unfortunately, you can't be sure that receiving one of these facility codes really means the object has died. You might receive a failure with one of these facility codes when the parameters of a method call couldn't be marshaled to or from the object. In such a case, the next call to the object might be successful.

After you determine the facility code, you should look at the error code in the lower 16 bits of the HRESULT if you really want to know what happened. Here's a function that looks at the lower 16 bits of the HRESULT and ignores everything else:

 Function GetErrorCode(ByVal HRESULT As Long) As Integer     GetErrorCode = (HRESULT And 65535) End Function 

Once you find both the facility code and the error code, you can look up the error in the header file WINERROR.H, which is part of the Win32 SDK. This file should include all system-generated HRESULTs. As you can imagine, working at this level can be very tedious for the average Visual Basic programmer. Remember that you won't have to work with HRESULTs very often—only when you receive an error generated by the system and the Visual Basic runtime doesn't know about it.

COM Exceptions

An HRESULT can convey whether a method has succeeded. It can also contain an application-specific error code. But it can't convey a text-based error message or any information about where the error actually occurred. You can use COM exceptions to pass a richer set of error information from an object back to its clients. Support for COM exceptions is provided by a handful of functions and interfaces in the COM library.

Before examining the inner workings of COM exceptions, you should note that all these details are hidden from Visual Basic programmers. The Visual Basic team took Visual Basic's proprietary error-handling model and cleanly mapped it on top of COM exceptions. As a result, Visual Basic programmers don't have to know a thing about COM exceptions.

Here's how COM exceptions work. Any COM object that throws exceptions must advertise this fact by supporting a standard interface named ISupportErrorInfo. This interface allows a client to query the object on an interface-by-interface basis to see whether the object supports exceptions for a specific IID. The object can tell the client, "Yes, I support COM exceptions for the interface you're using."

When an object wants to throw an exception, it must create an error object by calling a function in the COM library named CreateErrorInfo. This function creates the error object and returns an ICreateErrorInfo reference. The object then uses this reference to populate the newly created error object with information such as a description and an error source. Next, the object must call SetErrorInfo to associate the error object with the logical thread of the caller. (The term logical thread refers to a chain of method calls that might span several physical threads.) Finally, the object must return an HRESULT that tells the client that something has gone wrong.

An exception-savvy client can query an object to see whether it supports the ISupportErrorInfo interface. If it does, the client calls a method through ISupportErrorInfo to test for support on the interface that the client is currently using. If the object indicates that it supports exceptions on that interface, the client can be sure that an error object will be generated with contextual information about the nature of the problem whenever a failed HRESULT is returned. So, if the client inspects an HRESULT and finds that a method call was not successful, it calls GetErrorInfo to retrieve an IErrorInfo interface reference to the error object. This interface lets the client retrieve all the error information sent by the object. As you can see, COM exceptions require a lot of work on both sides. It's fortunate that Visual Basic does all this work for you behind the scenes but still lets you participate in this language-neutral error-handling scheme.

One point you should understand is that COM exceptions are pretty efficient. If an object raises a COM exception and the client is on another machine, all the error information is sent along the wire as the call returns. There's no need for a second round-trip between the client computer and the server computer to pick up extra error information. This relative ease is made possible by optimizations to COM's remoting layer. The contextual information associated with a COM exception is always marshaled across the wire with the method's response. The client can simply retrieve all this error information from a local cache.

Visual Basic errors are mapped to COM exceptions

If you have used Visual Basic's error-handling model, you know that it's much easier to use than COM's exception-handling model. It's fortunate that the two models work so well together. Visual Basic programmers continue to raise and trap errors in the same way that they have for years, and the details are transparently mapped by the runtime layer to conform to the language-independent model required by COM.

Every Visual Basic component has built-in support for ISupportErrorInfo. When you explicitly raise an error from a Visual Basic server with Err.Raise, the runtime layer automatically creates and populates a COM error object. It also makes the other necessary calls to properly throw a COM exception. All you really need to do is provide code, as shown in the following example:

 Dim MyErrorCode As Long, Source As String, Description As String MyErrorCode = vbObjectError + 512 Source = "DogServer.CBeagle.Bark" Description = "The cat's got the dog's tongue." Err.Raise MyErrorCode, Source, Description 

When you call Err.Raise, you must pass a user-defined error code. Visual Basic provides the intrinsic constant vbObjectError as the starting point for your user-defined error code. The constant vbObjectError maps to a failed HRESULT with a facility code of FACILITY_ITF. However, the COM team recommends that you actually start your custom-defined error codes at vbObjectError + 512 to avoid any conflicts with Microsoft-defined error codes.

The previous example also shows how to populate the Source and Description properties of an error object. The source should tell the client application where the problem occurred. This information is most useful during debugging. The description should give the client an indication of what went wrong in a human-readable form. It's common practice to display the description to the user, so keep that in mind. If the error condition has a remedy, it's often valuable to make that known in the description as well.

All the user-defined errors raised by your servers should have corresponding error codes. The error codes for a server should be published in the type library using enumerations. You can define a set of error codes in the following manner:

 ' Dog Server Error Codes Enum ErrorCodesEnum     dsDogUnavailable = vbObjectError + 512     dsDogUnagreeable     dsDogNotCapable     dsDogNotFound     dsUnexpectedError End Enum 

Visual Basic's mapping of COM exceptions on the client side is just as transparent as it is on the object side. When a method returns an HRESULT indicating failure, the mapping layer uses the ISupportErrorInfo interface to determine whether the object supports COM exceptions. If it does, the mapping layer retrieves the COM error object and uses it to populate a standard Visual Basic Error object. The Visual Basic runtime then raises an error back to the caller. You can use a Select Case statement in a client application to create a handler, like this:

 Sub MakeDogBark(Dog As IDog)     On Error GoTo MakeDogBark_Err     Dog.Bark ' Exit if successful. Exit Sub ' Enter handler on error. MakeDogBark_Err:     Select Case Err.Number         Case dsDogNotCapable:             ' Perform necessary handling in client.             MsgBox Err.Description, vbCritical, "Error: Dog Not Capable"         Case Else             ' Always provide a last-chance handler.             MsgBox Err.Description, vbCritical, "Unexpected Error"     End Select End Sub 

It is also acceptable for your components to raise errors based on HRESULTs defined by Microsoft. For example, if you conduct a runtime test in one of your methods and determine that the caller doesn't have the proper authorization to perform a task, you should raise an error. However, instead of defining your own custom error code, you should use the standard HRESULT that indicates that access has been denied. Here's an example of how to use this HRESULT in your code:

 Const E_ACCESSDENIED = &H80070005 Err.Raise E_ACCESSDENIED, , "Access has been denied" 

You can look at the file WINERROR.H to see what other HRESULTs Microsoft has defined. You also might have noticed that the previous code example defines the HRESULT constant using a hexadecimal format. You can also use the equivalent decimal value -2147024891. Either way is fine. It's just that WINERROR.H uses the hexadecimal format, which makes it easier to copy and paste these values into your Visual Basic source code.

Error-Raising Conventions

When you distribute COM servers, you are expected to follow the rules defined by COM as well as the rules used by Visual Basic programmers. This expectation is especially firm if you work in environments in which some components and clients are written with tools and languages other than Visual Basic. Here are some rules that you should keep in mind:

  • All COM exceptions that leave your server should be explicitly raised by your code. This means that only errors explicitly generated with Err.Raise should leave your server. You must prevent any unhandled errors from leaving your server. Most Visual Basic programmers know that an unhandled error in a client application results in the application's termination. A Visual Basic server, on the other hand, deals with an unhandled error by passing it to the client. For instance, if a method in your server experiences a division-by-zero error that isn't handled, the runtime layer simply forwards the error to the client application.
  • It's considered bad style to let exceptions leave your server that haven't been explicitly raised. The rules of COM state that any COM object that implements ISupportErrorInfo can pass only its own exceptions. To follow these rules, you must catch any error generated by methods such as those in the Visual Basic runtime and the ADO library. If your server experiences an error that it can't deal with, raise an application-specific "unexpected error" exception back to the client.

  • Always include a helpful text description when you raise an exception. Try to create informational descriptions at the place where the error occurred. The procedure in which the error occurs usually has the most contextual information. It's easy to propagate this description back to the original caller from anywhere in the call chain. A description such as "Order for $2000.00 could not be accepted. The customer has a credit balance of only $1250.00" is far more valuable than "Order submission failure." The handler in the client application often displays this error message to the user. Remember that error codes are for programmers and that error descriptions are for users.
  • Always use enumerations to define custom error codes. This practice will allow other programmers to see your error codes through the type library. Visual Basic programmers using your server will be able to see these error codes with the Object Browser.
  • Always define custom error codes using vbObjectError + 512. The lower error codes are reserved by the COM team and have special meanings. If you use the vbObjectError constant and add 512, you can be sure that your codes won't be misinterpreted as system-defined error codes.
  • Try to supply documentation with your servers that describes errors to other programmers. You can convey only so much information with an enumerated value such as dsDogNotCapable. If possible, you should include a list of remedies and workarounds for each error condition.
  • Never display a message box in your server's error handlers. The decision to interact with the user should always be left to the client application. With an out-of-process server, an error handler that displays a message box will hang the server, and that is something you want to avoid. Although you can display a message box to the user from an in-process server, doing so is considered bad style. The client application programmer should ultimately control all interaction with the user.

Handling Errors in Scripting Clients

Your component's clients might be written in ASP scripts or as Microsoft Windows Script Host (WSH) files using languages such as VBScript and JavaScript. If you're writing components for scripting clients such as these, you can and should raise errors in the manner just described whenever a method can't complete its work. This means that scripting clients shouldn't be treated any differently than other clients. However, you should be aware that the manner in which the error-handling code is written in a scripting client is not very elegant. Let's look at an example of a client written in VBScript:

 ' VBScript in a WSH file Const dsDogNotCapable = &H80040202 On Error Resume Next Dim Spot Set Spot = CreateObject("DogServer.CBeagle") ' Call method. Spot.Bark ' Test for success or failure. Select Case Err.Number     Case 0         ' Things are good. The call did not experience an error.     Case dsDogNotCapable         MsgBox "The dog isn't capable of barking at this moment"     Case Else         MsgBox "Unexpected error #" & Err.Number & ": " & Err.Description End Select ' Be sure to clear error object. Err.Clear ' Now call other methods and test for errors in similar fashion. 

If your component uses enumerations to publish error codes, a scripting client usually can't use this information. One easy way to deal with this problem is to simply use constants in the scripting clients to map error codes to readable names. Also note that you are limited to the On Error Resume Next statement when you handle errors in VBScript. This means you must inspect Err.Number after each method call. If Err.Number is not equal to zero, a runtime error has occurred. Also note that once you handle a runtime error, you must clear the Err object using the Clear method before attempting another call.

Summary

This chapter covered the basics of designing and building servers with Visual Basic. You must first decide whether to package your components in ActiveX DLLs or ActiveX EXEs. The way you package your code affects your server's performance, robustness, and security. Many distributed application designs require that your objects be deployed in an out-of-process server. However, sophisticated runtime environments such as COM+ and IIS provide a surrogate process, which means that you still distribute your code in an ActiveX DLL.

As you've seen, Visual Basic automatically builds lots of support into your servers at compile time. The Visual Basic compiler and runtime layer work hard to hide many of the details required by COM. Your job as a component author is to concentrate on how to expose functionality through interfaces defined in the type library and how to provide an implementation of those interfaces in your components.

This chapter discussed advanced techniques such as using user-defined interfaces, enumerations, and UDTs when you design your components. You also saw that many of these techniques create problems when scripting clients are involved. It's best if you can make assumptions early in the design phase about which types of clients will use your components.

This chapter also exposed you to the important issues of raising and handling errors. It's comforting that Visual Basic provides a transparent mapping on top of COM exceptions. Visual Basic objects and clients can participate in exception handling with components written in other languages. In most situations, Visual Basic hides all the gory details of HRESULTs and COM exceptions, so you can raise and handle errors in the manner that Visual Basic programmers have always used.



Programming Distributed Applications with COM+ and Microsoft Visual Basic 6.0
Programming Distributed Applications with Com and Microsoft Visual Basic 6.0 (Programming/Visual Basic)
ISBN: 1572319615
EAN: 2147483647
Year: 2000
Pages: 70
Authors: Ted Pattison

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net