Data Management

How to log errors in Microsoft Access

If you Google "error logging in Microsoft Access," you'll find a number of complex solutions. But the process doesn't have to be that difficult. Most of us need only a simple function to log specific information about the current error.

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"
End Function

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

SQL's INSERT INTO statement works well as long as you use it correctly:

  • 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:

ErrDate: Date/Time
CompName: Text
UsrName: Text
ErrNumber: Numeric
ErrDescription: Text
ErrModule: Text

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.

Figure A

figure a

Clear the informational message.

Figure B

figure b

The table shows the new error record.

A project-level routine

Inserting 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
End Function

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"
End Function

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.
An ActiveX Data Object (ADO) Recordset presents a more versatile solution than pure SQL. The code in Listing D (basErrorLog3.bas in the download) uses ADO to log errors.

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
End Function

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
  Err.Raise 5
  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
End Sub

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.

Figure C

figure c

Let users know when the VBA is unable to log an error.

Worth noting

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 independent consultant and the author of several articles and books on database technologies. Her most recent book is Mastering Microsoft SQL Server 2005 Express, with Mike Gunderloy, published by Sybex. Other collaborations with Gunderloy are Automating Microsoft Access 2003 with VBA, Upgrader's Guide to Microsoft Office System 2003, ICDL Exam Cram 2, and Absolute Beginner's Guide to Microsoft Access 2003, all published by Que. Currently, Susan volunteers as the Publications Director for Database Advisors. You can reach her at ssharkins@gmail.com.

About

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.

4 comments
chuck
chuck

I have made a bit of a modification to the code and included it im my most current development project. In the error trapping routine I have added the following code: cmdOpen_Click_Error: Dim vlogDesc As String vlogDesc = "Error " & Err.Number & " (" & Err.Description & ") in procedure cmdOpen_Click of VBA Document Form_frmReportSelection" Call LogError(vlogDesc) MsgBox vlogDesc & " has been logged!" With this logic the logerror function captures the name of the routine that generated the error. I have also sent this code to the Microsoft Access Developers user group in Dallas, Tx for their comments and suggestions.

chuck
chuck

Where is the associated download?

ssharkins
ssharkins

The download window's empty for me too -- however, in-house it's working. They're looking into it. E-mail me privately at ssharkins @ gmail dot com and I'll send you the .bas file.