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.
- Retry the operation. If you can't overwrite a file because it's read-only, the user can possibly fix this herself and retry it.
- Ignore the error and try to continue execution. This is often a practical solution, provided that the code copes with the incompletely executed statement.
- Always ignore this error. This option is very handy if the same error keeps coming up repeatedly. This could happen if the error occurs in a loop or a recurring event, such as a form's Paint event or a Timer event. Without the option to Always ignore an error, the only way to survive is to quit the program.
- Quit the program. If nothing else helps, there should be a safe way out. Ideally, this option closes open files and database connections and also frees any used resources.
- Report the error to the developer(s) or log it automatically.
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?
- Error message and/or number are crucial. If you don't have either, you don't know what the fault was. Unfortunately, the user doesn't necessary know the value of this information and all you get is a vague crash report.
- Exact location is a key to fixing the error. If you don't know which statement failed, you have few ideas about what to change. If you know the line, you're much better off. Getting line of error requires that you use line numbers in your code, which isn't standard coding practice these days.
- Program version. Simple but not always always at hand. If you know you fixed a certain problem back in v1.2.3, you can immediately tell the user to replace the old version with the current one, keeping both of you happy.
- Call stack. A problem might happen only during a special call sequence. Say a function a the root of the call tree fails to instantiate an object and your app crashes when trying to use it. All other caller sequences work fine but this one sequence doesn't. Knowing the call stack lets you focus on the code that is likely to contain the root cause of the fault. This is especially useful in a big system with complex call paths.
- Variable values. Knowing variable values at the time the error hits often proves indispensable. Just to name an example, if
a=b*c
produces an Overflow, how do you know what the problem is? You need to verify both b and c to determine which one of them has an unexpectedly large value. Since VB can store values in several locations, the problem value might lie in a parameter, local variable, module-level or global variable, array, field of a user-defined type or even property of an object. - Screenshot is useful to understand what status the application was in.
- System information comes handy especially when you can't reproduce the bug on your machine and it seems to be related to the operating system or some run-time file. You can possibly utilize information about OS and Service Pack version, installed run-time files and their versions, available disk space, screen resolution, other running processes, available fonts etc.
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.