Visual Basic error handling

Caution! A run-time error can cause data loss, user aggravation and severe developer headache.

When an error hits the user, she runs the risk of losing unsaved data. If it's not her lucky day, her computer jams and the database corrupts. The system won't start up again. There is no backup. Naturally, she calls you (or your boss) yelling and demanding immediate action. You don't even know the error message. You get sick of such a user and never want to deal with her again. Tired of problems, you quit your developer career and start making burgers instead.

Benefit from errors

There's nothing positive about errors, is there? How about this way to look at it: When an error hits the user, your application reacts to it in a reasonable way, protecting the data and reporting the error to you. You find the cause of the fault and provide a fix in a short time. The user is amazed by your performance and buys a new project from you. You're the best choice since not only is your software great but you also give the best service.

This kind of a paradise is not so far from the reality. With proper error handling you protect the users and get extensive information for fixing the bugs.

This article is written with Visual Basic 6.0 in mind. The concepts presented are universal and not tied to a specific language or environment. VB Watch Protector is an automated tool that provides VB applications with the error handling features suggested by this article.

What should your app do when an error occurs?

When a run-time error occurs, the default way for Visual Basic to handle it is to display an error message and crash. Would you design your apps this way?

Instead of the default way, you should trap the error, display a detailed description of what happened and give the user some options to cope with the failure.

Depending on the case, you could also offer extra options such as try another feature, reopen a connection, override file protection, free up some resources or even a big PANIC button to call for help.

Information about the error

To fix errors you need as much information about them as possible. If often happens that the error message alone isn't enough to locate the error or even understand what went wrong.

You need more details. Unfortunately, you don't get it for compiled apps that easily. All you get is the error message. How about the name failing module? Procedure?

Implementing proper error handling

Error handlers should cover all the lines, not just the most error-prone chunks. Not a single line should go unprotected unless you're sure it can't possibly fail under any circumstances. Even the shortest event handler can make you app crash, either by calling other functions or triggering other events.

Deferred error handling

One way is to use deferred error handling with the statement On Error Resume Next. This statement clears the special Err variable. When an error occurs, VB stores the error details in the Err variable and continues execution as if there was no error. You can then examine the variable to see what happened.

On Error Resume Next ' Err is set to zero
Kill "file1.txt"
Kill "file2.txt"
Open "file1.txt" For Output As #1
If Err <> 0 Then
  ' File operation(s) failed, handle the error
  ...
End If

This approach looks quite simple but it has some drawbacks. Probably the most important one is that your error handler doesn't necessarily execute immediately after the error. In the above example, you can't easily tell which of the statements failed. You will need to add error checks at frequent intervals to catch every possible error source.

Combined local and global error handler

The preferred way to implement error handling is to provide a short local error trap and call a global error handler that does the rest. In VB6 you do this with the statement On Error Goto line. This way you can reuse most of the logic throughout the program.

Sub FileOperations()

On Error Goto LocalHandler
Kill "file1.txt"
Kill "file2.txt"
Open "file1.txt" For Output As #1
Exit Sub

LocalHandler:
' Handle any error(s) here by calling
' GlobalErrorHandler with appropriate parameters

Select Case GlobalErrorHandler(...)
  Case errQuit
     ' Quit immediately after some cleanup
     Quit
  Case errRetry
     ' Retry execution of failed statement
     Resume
  Case errIgnoreLine
     ' Ignore fault, resume execution at next statement
     Resume Next
End Select

End Sub

Here, GlobalErrorHandler is a function that takes status information (such as procedure name) as input. It displays an error message, produces an error report, logs the error (or does just one of these depending on what you want) and returns a value telling how to proceed. Depending on the return value the program either stops, retries the operation or just continues ignoring the faulty line.

By changing GlobalErrorHandler you can provide different error handling for a database app, a server component or a control library. For example, you can record errors to a database but resort to a log file if no connection is available. You can store all relevant error information in a .zip file and offer the user an option to send it to you. If the application doesn't have a user interface, then you might want to defer showing an error dialog and try some default action. For example, you could wait for 5 seconds then retry, and if it doesn't work, quit after trying 3 times. You could add sophisticated logic for specific errors: if the error looks like there is no connection, the error handler could try opening the connection and continuing without even telling the user anything went wrong. You could also implement the Always ignore feature mentioned earlier in this article by ignoring a given error code on a given line while displaying a dialog for every other error.

You can do all of this by simply tuning GlobalErrorHandler. If the local handlers are properly designed, you don't necessarily have to modify them at all to provide better recovery from errors.

Line numbering

There's one more important thing to add: line numbers. This is the only way to know for sure which line failed.

On Error Goto ErrorHandler
10 Kill "file1.txt"
20 Kill "file2.txt"
30 Open "file1.txt" For Output As #1

That doesn't look nice, or does it? Most developers don't expect to write line numbers these days. Fortunately, you can use an automated tool to add them before you compile the program. This way you keep working on the unnumbered code but get line numbers in your error messages (by reading the value of Erl).

Automating the writing of error handlers

Since a majority of the procedures are going to be served by a similar error handler, it makes sense to automate the process of adding those handlers and line numbers. VB Watch Protector is a tool that contains features for that purpose. It generates a copy of your source and adds error handling code. You can use the predefined advanced error handlers or write your own to fit your use.

With VB Watch, any existing error handlers remain in effect. This way you can write code for the expected errors (such as file or database access errors) manually and automate the less critical locations.

If you want to manually write the error handlers, you could still use some programmatic help. Project Analyzer lists procedures missing an error handler, plus ones with just an On Error Resume Next. You can use it for making a code review: take a look at each reported procedure to determine whether an error handler should be built or not.