Despite all your best efforts, errors occur in every database. Most developers include adequate error-handling routines in their code, but that might not be enough. Knowing when an error occurs and how often it occurs can be important to resolving the issue and avoiding future errors. Access doesn't track errors, but you can add that functionality to any database. All you need is a table and a little code.
Note: Our companion download includes a demonstration database and a several .bas module files you can import into any Access database. Don't cut and paste the code directly from here into an Access module, as the article text contains formatting that will generate errors.
The easiest way
If you have just one database to maintain and you're its only user, error logging isn't a critical issue because you're around when the error occurs. However, it's difficult to display internal error information at the time of the error. It isn't impossible, but even if your application displays it, you have to remember all of it. For that reason alone, adding a log can be helpful, especially during the testing stage.The simplest way to log errors is to let each procedure's error-handling routine do it. The error routine shown in Listing A (basErrorLog1.bas) inserts a record with error information into an existing table. To create the code, select Module in the Database window and then click New on the Database toolbar. Enter the code and save the module. (You can import the .bas file into any Access database, even an empty one.)
Listing A: ThrowError()
Function ThrowError() On Error GoTo errHandler <p>Dim strSQL As String</p> <p>Dim strDescription</p> <p>Err.Raise 6</p> Exit Function
errHandler: strDescription = Chr(34) & Err.Description & Chr(34) <p>strSQL = "INSERT INTO tblErrorLog (ErrDate, CompName, UsrName, " _</p> <p>& " ErrNumber, ErrDescription, ErrModule)" _</p> <p>& " VALUES(#" & Now() & "#, '" & Environ("computername") _</p> <p>& "', '" & CurrentUser & "', " & Err.Number _</p> <p>& ", " & strDescription & ", '" & VBE.ActiveCodePane.CodeModule & "')"</p> <p>DoCmd.SetWarnings False</p> <p>DoCmd.RunSQL strSQL</p> <p>DoCmd.SetWarnings True</p> MsgBox "Error has been logged", vbOKOnly, "Error"
The function intentionally throws an overflow error in order to execute errHandler. This code builds a SQL INSERT INTO statement using the following information: the date, the computer's name, the user's name, the internal error number and description, and the module that generated the error.
About INSERT INTO
- The target table (in this case that's the error log table) must contain every field in the INTO and VALUES clause.
- You can omit a field from the INTO clause as long as you also omit it from the VALUES clause; these two clauses must match.
- When including field references in the INTO clause, their order must match their placement in the table (check the table in Design view, which won't always agree with Table view).
- Don't include an AutoNumber field in either clause.
- Jet won't complain if there's no data for a specified field, unless doing so violates a field constraint. That behavior could help or hinder.
- You can omit all of the fields from the INTO clause, but you must account for every field in the target table in the VALUES clause.
You can customize the information to fit your needs. For instance, if no one else is using the database, you won't need the user or computer names. Just remember to delimit each item correctly. Delimiters cause a bit of a problem for the error's description text, as quite often this text includes single quotation marks. Assigning the text to a string variable, separate from the SQL string variable, makes the text easier to handle.
You must add this capability to every procedure to maintain a comprehensive log. In addition, before executing the code, create the error log table. The name of the example's table is tblErrorLog, and it contains the following fields:
UserName and Computer Name are reserved words; even though those labels are more meaningful, you can't use them to name a field or variable.You can execute the code from inside the VBE. To do so, position the cursor anywhere inside the procedure's code and press F5. Clear the message shown in Figure A. Open tblErrorLog to see the error record shown in Figure B.
Clear the informational message.
The table shows the new error record.
A project-level routineInserting error -logging code into every procedure works, but it's inefficient. Consider calling a dedicated function, like the one in Listing B, instead. Call LogError() from each procedure's error handling routine, as the procedure in Listing C shows. (Listing B and Listing C are in the download as basErrorLog2.bas.)
Listing B: LogError()
Public Function LogError() 'Log error information to tblErrorLog. <p>Dim strDescription As String</p> <p>Dim strSQL As String</p> <p>strDescription = Chr(34) & Err.Description & Chr(34)</p> <p>strSQL = "INSERT INTO tblErrorLog (ErrDate, CompName, UsrName, " _</p> <p>& " ErrNumber, ErrDescription, ErrModule)" _</p> <p>& " VALUES(#" & Now() & "#, '" & Environ("computername") _</p> <p>& "', '" & CurrentUser & "', " & Err.Number _</p> <p>& ", " & strDescription & ", '" & VBE.ActiveCodePane.CodeModule & "')"</p> <p>DoCmd.SetWarnings False</p> <p>DoCmd.RunSQL strSQL</p> DoCmd.SetWarnings True
Listing C: ThrowError()
Function ThrowError() On Error GoTo errHandler <p>Err.Raise 6</p> Exit Function
errHandler: Call LogError MsgBox "Error has been logged", vbOKOnly, "Error"
When ThrowError() experiences an error, VBA passes that error to errHandler, which calls LogError(). The LogError() function inserts a new record using the error information into tblErrorLog and then returns to ThrowError().
A more compact solution
The one disadvantage to using pure SQL is that the statement can grow rather long and unwieldy. If you're not comfortable writing SQL, this solution might be a little difficult for you. There are other reasons to avoid SQL:
- If there are any plans for upsizing to SQL Server, avoid using Jet SQL, as SQL Server uses Transact-SQL (T-SQL). You'll have to modify the SQL code so that SQL Server can use it.
- If you're using Access to work with SQL Server tables, the Jet SQL will fail. You could use a Pass-Through query, but you must be familiar with T-SQL.
Listing D: LogError()
Public Function LogError() 'Log error information to tblErrorLog using ADO. <p>Dim cnn As New ADODB.Connection</p> Dim rst As New ADODB.Recordset
Set cnn = CurrentProject.Connection rst.Open "SELECT * FROM tblErrorLog", cnn, _ adOpenKeyset, adLockOptimistic
With rst .AddNew <p>!ErrDate = Now()</p> <p>!CompName = Environ("computername")</p> <p>!UsrName = CurrentUser</p> <p>!ErrNumber = Err.Number</p> <p>!ErrDescription = Err.Description</p> <p>!ErrModule = VBE.ActiveCodePane.CodeModule</p> <p>.Update</p> End With
rst.Close Set rst = Nothing Set cnn = Nothing
Execute ThrowError(), and the results are the same. The only difference is that the ADO version is more versatile because it will run against a SQL Server backend, as long as you establish the right connection (update the CurrentProject.Connection shortcut the example uses, appropriately).
Add error handling to the error logging
So far, the error-logging routines haven't included error handling. For that reason, if you add one of these solutions as is, thoroughly test it to make sure you accommodate all possible errors.You might want to add error handling to the error-logging routine, because any error within the error-logging routine will write over the current error properties you're trying to log. Passing the error-logging information as shown in Listing E is a simple solution. As with all the previous examples, call the error logging function by executing ThrowError() in Listing F. (Listings E and F are in download file as basErrorLog4.bas.)
Listing E: LogError()
Public Function LogError(errdte As Date, comname As String, _ usernme As String, errno As Long, errdes As String, errmod As String) As Boolean <p>'Log error information to tblErrorLog using ADO.</p> <p>Dim cnn As New ADODB.Connection</p> <p>Dim rst As New ADODB.Recordset</p> <p>Dim varErrors As Variant</p> <p>Dim varFields As Variant</p> On Error GoTo errHandler
varFields = Array("ErrDate", "CompName", "UsrName", _ "ErrNumber", "ErrDescription", "ErrModule") varErrors = Array(errdte, comname, usernme, errno, errdes, errmod)
Set cnn = CurrentProject.Connection rst.Open "SELECT * FROM tblErrorLog", cnn, _ adOpenKeyset, adLockOptimistic
rst.AddNew varFields, varErrors rst.Update
rst.Close Set rst = Nothing <p>Set cnn = Nothing</p> <p>LogError = True</p> Exit Function
errHandler: Debug.Print errdte <p>Debug.Print comname</p> <p>Debug.Print usernme</p> <p>Debug.Print errno</p> <p>Debug.Print errdes</p> <p>Debug.Print errmod</p> <p>Debug.Print Err.Number</p> Debug.Print Err.Description
Set rst = Nothing Set cnn = Nothing <p>LogError = False</p> End Function
Listing F: ThrowError()
Sub ThrowError() On Error GoTo errHandler <p>Dim booLogError As Boolean</p> <p>Err.Raise 6</p> Exit Sub
errHandler: booLogError = LogError(Now(), Environ("computername"), CurrentUser, _ <p>Err.Number, Err.Description, VBE. _</p> <p>ActiveCodePane.CodeModule)</p> <p>If booLogError Then</p> <p>MsgBox "Error has been logged", vbOKOnly, "Error"</p> <p>ElseIf booLogError = False Then</p> <p>MsgBox "Error wasn't logged", vbOKOnly, "Error"</p> End If
This version of ThrowError() passes the current error information to LogError(), which purposely throws another error in order to execute its error-handling routine. Of course, when applying this to your own work, you'll want to delete LogError()'s Err.Raise statement. The error-handling routine prints the error values only to verify that the original error values still exist, even after a new error occurs. How you handle those values will be up to you. Just remember, as is, their scope is specific to LogError(). Once VBA returns flow to ThrowError(), they're gone. The array presents an easy but alternate way to update the log table. Stick with the ADO solution if you're working with SQL Server tables.Notice that LogError() is now a Boolean function procedure (no longer a subprocedure as before). If VBA successfully logs the error, the function returns True; if not, the function returns False. The calling subprocedure (Listing F) contains an enhanced If statement that returns the appropriate message, depending on whether LogError() returns True or False. Figure C shows the simple message shown when the code fails to log an error. When you apply this technique to your own procedures, you'll want to include specific error handling. As is, LogError() merely maps the flow for you.
Let users know when the VBA is unable to log an error.
This is a good procedure to add to your code library. Just drop it into any database. If you're working with an older database, make sure to reference the ActiveX Data Objects library (ADO).
Some developers don't like Environ(); they complain that it's unstable. If you have trouble with this function and you need to track the computer, use the API GetComputerNameA() function instead.
Remember, you must create the log table manually. You can, however, add code that creates the table if it doesn't already exist.
Easy error log
If you Google error logging in Microsoft Access, you'll find a number of complex solutions. The process doesn't have to be hard to be good. Most of us need only a simple function to log specific information about the current error, for later use.
Susan Sales Harkins is an IT consultant, specializing in desktop solutions. Previously, she was editor in chief for The Cobb Group, the world's largest publisher of technical journals.