I've got a set of fairly complicated table validation rules that I'm using to keep my data clean in one of my MS Access tables. The rules appear to be working well, but when a rule is violated it generates a nasty-looking error message. The whole string of rules comes up in the message - not exactly user-friendly.
I've tried to replace the message using code similar to this in the form's OnError event:
Private Sub Form_Error(DataErr As Integer, Response As Integer)
If DataErr = 2601 Then
MsgBox ("Case Number already entered")
Response = 0
End If
End Sub
However, apparently I haven't managed to find the correct error number because the If statement never fires. I checked here, but apparently it's not 3317 or 4001.
Also, when I removed the If statement and just had the MsgBox fire on any error, the standard error came up along with the message box - it's not being replaced.
Any idea how to address these issues?
Related
I'm using the form_error event in MS Access to catch errors, which gives only a dataerr integer and not a description like a vba error, however the MS Website gives the following description of the event:
You can use the DataErr argument with the Error function to map the
number to the corresponding error message
But doesn't actually give an example of this and I'm struggling to figure it out.
The specific situation is that the user will be updating records in a subform, some fields may break referential integrity rules, the number will only tell me that a referential integrity rule has been broken, whereas the description will give me the table name, which allows me to give a prescriptive error message relating to the field where the error occurs
Is there an easy way to do this?
Create a separate function to perform all of your validation and message the user with anything missing. Run that in the form's beforeupdate event and cancel the event if needed. That would probably eliminate many of your form_error's. In the form_error event, you could call the same function. It is has found an issue, suppress the default message. Any other error that pops up you'll can handle separately if needed.
Normally it is just to pass the number to Error to obtain the description:
MsgBox Error(DataErr)
In short
As far as I know #AVG's solution is probably the best available workaround, with one exception. That is, #AVG probably gets it right that the best workaround is to: try to catch the error at the Form_BeforeUpdate event; and (probably, depending on your needs) cancel the event in Form_BeforeUpdate.
However, it is a good idea not to suppress the default error at the Form_Error level. If you catch and cancel the error at Form_BeforeUpdate then the error won't arise at Form_Error. So there's no need to suppress the error at Form_Error in that case. But if there's another kind of referential integrity violation laying in wait (e.g. an Category/Organisation violation rather than a Category/Subcategory violation), that you haven't thought to handle as a developer, then having the default error message arise in Form_Error will alert you to it. You wouldn't want to suppress a message for an error you haven't caught.
Below I provide example code for this workaround.
I also discuss how AccessError(DataErr) looks like a possible solution, but isn't quite. That is, it can be used to map DataErr numbers to messages. This is partially helpful. But, unfortunately, the messages are the variable unsubstituted versions. So, for example, we can't use AccessError(DataErr) in code to interrogate specific referential integrity violation messages.
Making the problem sharper
A Form_Error event will fire when a user, through a form: violates referential integrity (that you've set precisely to prevent such attempts at invalid data states); violates a validation rule set at the table level; or otherwise (?) produces a data error.
However, the default message returned is likely to be user-unfriendly. E.g., in the case of a referential integrity violation ...
You cannot add or change a record because a related record is required in table 'CategorySubcategory'.
In this example the table 'CategorySubcategory' expresses a many-to-many relationship between tables 'Category' and 'CategorySubcategory', defining the particular Subcategories that belong to particular Categories. (In my example) it is also expressed, in CategorySubcategory, that some of the Categories have no Subcategories.
What's needed is a means to have a user friendly message in place of the user-unfriendly message. And to do this it would be convenient to interrogate the default error message as it arises, suppress it, and substitute your own.
(As far as I know) the closest we can come to interrogating the default message, in Form_Error, is AccessError(DataErr). Unlike Error(DataErr), AccessError promises to
Microsoft > Docs > Office VBA Reference > Access > Object model > Application object > Methods > AccessError
... return the descriptive string associated with a Microsoft Access or DAO error [Emphasis added]
[And the promise is that you] can use the AccessError method to return a descriptive string from within a form's Error event.
So you might be tempted to do something like, and it would be convenient if you could do something like ...
Private Sub Form_Error(DataErr As Integer, Response As Integer)
If AccessError(DataErr) Like "*CategorySubcategory*" Then
MsgBox "[My user friendly message] Choose the right Subcategory given the Category; or change the Category.", vbOKOnly Or vbExclamation
Response = acDataErrContinue ' Suppress default message
End If
End Sub
Might AccessError work?
However, it turns out AccessError only returns the variable unsubstituted version of the error message, not the variable substituted message the user sees at run-time. For example AccessError returns something like ...
You cannot add or change a record because a related record is required in table '|'.
... rather than ...
You cannot add or change a record because a related record is required in table 'CategorySubcategory'.
To prove this trigger a data error (E.g. by violating referential integrity) with the following.
Private Sub Form_Error(DataErr As Integer, Response As Integer)
' For some reason you'll need to call AccessError(DataErr) before Error(DataErr)
Debug.Print "AccessError(DataErr): ", AccessError(DataErr)
Debug.Print "Error(DataErr): ", Error(DataErr)
End Sub
Observe both: AccessError returns a specific message where Error returns the uselessly generic 'Application-defined or object-defined error'; and AccessError returns the variable unsubstituted version of the error message.
#Hello World correctly quotes from MS Docs > ... > Form.Error event (Access)
You can use the DataErr argument with the Error function to map the number to the corresponding error message
Even with AccessError substituted for Error #Hello World's lament remains fair ...
[The MS docs don't] actually give an example of this and I'm struggling to figure it out.
Mapping messages to the DataErr number
What the MS doc author probably has in mind is code like the following
Function AccessAndJetErrorsTable() As Boolean
' Source: Access 2000 Help "Determine the Error Codes Reserved by Microsoft Access
' and the Microsoft Jet Database Engine"
Const strTable As String = "zttblAccessAndJetErrors"
Dim cat As New ADOX.Catalog
Dim tbl As New ADOX.Table
Dim cnn As ADODB.Connection
Dim rst As New ADODB.Recordset, lngCode As Long
Dim strAccessErr As String
Const conAppObjectError = "Application-defined or object-defined error"
On Error GoTo Error_AccessAndJetErrorsTable
Set cnn = CurrentProject.Connection
' Create Errors table with ErrorNumber and ErrorDescription fields.
tbl.Name = strTable
tbl.Columns.Append "ErrorCode", adInteger
tbl.Columns.Append "ErrorString", adLongVarWChar
Set cat.ActiveConnection = cnn
cat.Tables.Append tbl
' Open recordset on Errors table.
rst.Open strTable, cnn, adOpenStatic, adLockOptimistic
' Loop through error codes.
For lngCode = 0 To 35000
On Error Resume Next
' Raise each error.
strAccessErr = AccessError(lngCode)
DoCmd.Hourglass True
' Skip error numbers without associated strings.
If strAccessErr <> "" Then
' Skip codes that generate application or object-defined errors.
If strAccessErr <> conAppObjectError Then
' Add each error code and string to Errors table.
rst.AddNew
rst!ErrorCode = lngCode
' Append string to memo field.
rst!ErrorString = strAccessErr
rst.Update
End If
End If
Next lngCode
' Close recordset.
rst.Close
DoCmd.Hourglass False
RefreshDatabaseWindow
MsgBox strTable & " errors table created."
AccessAndJetErrorsTable = True
Exit_AccessAndJetErrorsTable:
Exit Function
Error_AccessAndJetErrorsTable:
MsgBox Err & ": " & Err.Description
AccessAndJetErrorsTable = False
Resume Exit_AccessAndJetErrorsTable
End Function
To use that code, create a new Database, put the code in a module, your cursor in the middle of the code, then hit F5 (to run it). A table zttblAccessAndJetErrors will be created where you can look up all the variable unsubstituted versions of the Access and Jet error messages; and correspond these to a DataErr number.
That code will give you the most up to date list of error messages. However, as an alternative you could just lookup an online source where this is published. E.g. FMS > Microsoft Access 2010 Error Numbers and Descriptions
So either with zttblAccessAndJetErrors or a lookup online you can now determine the DataErr number that corresponds most closely to the error message you are seeing during testing. So that gives us the ability to do something like ...
Private Sub Form_Error(DataErr As Integer, Response As Integer)
Select Case DataErr
Case 3201
' "You cannot add or change a record because a related record is required in table '|'."
MsgBox "[My user friendly message] Choose the right Subcategory given the Category; or change the Category.", vbOKOnly Or vbExclamation
Response = acDataErrContinue ' Suppress default message
End Select
End Sub
That is, we don't use AccessError directly in Form_Error to return a message. Rather we've used AccessError (or relied on someone else to have used) to produce the DataErr/Message mapping. In Form_Error we continue to match against the DataErr number only. But now we can copy the relevant message as a comment for our DataErr number.
However, at least in this referential integrity (DataErr 3201) case our problem remains. If we suppressed the default message for a specific referential integrity violation (like a Category/Subcategory mismatch), as with the code above, then that risks burying other specific referential integrity violations. E.g. If another referential integrity violation occurred with respect to, say a Category and Organisation chosen by the user, then the above code would wrongly express (beguilingly in a user friendly manner) that a Category/Subcategory violation had occurred.
The probable best available workaround
So, as far as I know #AVG's solution is probably the best available workaround, with one exception. That is, #AVG probably gets it right that the best workaround is to: try to catch the error at the Form_BeforeUpdate event; and (probably, depending on your needs) cancel the event in Form_BeforeUpdate.
However, it is a good idea not to suppress the default error at the Form_Error level. If you catch and cancel the error at Form_BeforeUpdate then the error won't arise at Form_Error. So there's no need to suppress the error at Form_Error in that case. But if there's another kind of referential integrity violation laying in wait (e.g. an Category/Organisation violation rather than a Category/Subcategory violation), that you haven't thought to handle as a developer, then having the default error message arise in Form_Error will alert you to it. You wouldn't want to suppress a message for an error you haven't caught.
So, in my case, the workaround code looks like (having to "go down the DLookup route" as #Hello World suggests).
Private Sub Form_BeforeUpdate(Cancel As Integer)
' Head off a Form_Error referential integrity error 3201:
' "You cannot add or change a record because a related record is required in table 'CategorySubcategory'."
Dim subCategoryIDGivenCategoryIDAtTableLevel As Variant
If IsNull(Me.cboSubcategory) Then
subCategoryIDGivenCategoryIDAtTableLevel = DLookup("[SubcategoryID]", "[CategorySubcategory]", "[CategoryID] = " & Me.cboCategory)
If Not IsNull(subCategoryIDGivenCategoryIDAtTableLevel) Then
' The user has not chosen a value for cboSubcategory, when they should have chosen some value
message = "For this Category you need to choose a Subcategory. Either choose a Subcategory or change the Category."
Me.cboSubcategory.SetFocus
Cancel = True
End If
End If
If Cancel Then
MsgBox message, vbOKOnly Or vbExclamation, GetApplicationTitle() & " (Record Level Validation)"
End If
End if
Private Sub Form_Error(DataErr As Integer, Response As Integer)
Select Case DataErr
Case 3201
' A referential integrity error ...
' "You cannot add or change a record because a related record is required in table '|'."
' We display the default error message in case we haven't caught a referential integrity
' condition in Form_BeforeUpdate
Response = acDataErrDisplay
End Select
End Sub
I always use the below code pattern so that users can get a proper error message, which they can then convey to me to understand the reason.
Private Sub cmdSearch_Click()
On Error GoTo Sub_Err
--Your code here---
'errors catching stuff
Sub_Exit:
Exit Sub
Sub_Err:
MsgBox Error$
Resume Sub_Exit
End Sub
Use the same thing for every sub you use in code.
Hello i am using a form in ACCESS to enter new data for a company. First of all the form shows me the current data with a query , and when i want to introduce a new data in the fiels in a line i press Tab many times and when ends it gave me an Error:
the data was added to the database but the data won't be displayed in the form because it
doesn't satisfy
and i close the form and when i open again the form for that company the values that i entered are there...
how can i solve this error , any ideas?
Add some rudimentary error trapping. If your code doesn't already contain it, you really need to read up on it. Any code without some error trapping is just flat out poorly written code.
To start, use a shell like this and modify it appropriately:
Sub|Function SomeName()
On Error GoTo Err_SomeName ' Initialize error handling.
' Code to do something here.
Exit_SomeName: ' Label to resume after error.
Exit Sub|Function ' Exit before error handler.
Err_SomeName: ' Label to jump to on error.
MsgBox Err.Number & Err.Description ' Place error handling here.
Resume Exit_SomeName ' Pick up again and quit.
End Sub|Function
If it hits an error, it will jump to the Err_SomeName: line and then display a messagebox with the error number and description. It will then exit the Sub/Function. What you really want to do is instead of just popping up a messagebox, do something that actually solves the problem. This is what error handling is all about, and it's critical to ensure your program functions exactly the way it's supposed to.
I'm building a relatively simple datasheet-style form for selecting a record (which will be used to populate another form)
The form pulls from a query with multiple columns. Since it's counter-intuitive to have a highlighted field and data entry cursor when all the user wants to do is select a record, I added the following code to each field on the form:
Private Sub Model_Enter()
RunCommand acCmdSelectRecord
End Sub
This works fine for the "Model" field, but for every other field it gives me the following error:
Run-time error 3125
" is not a valid name. Make sure that it does not include invalid characters or punctuation and that it is not too long.
There's literally nothing else going on in this form, so I have no idea what the issue is, or why the code works for Model and breaks for everything else. Anyone else run into this issue?
I suggest you use the On Current event, rather than write code for each control. It is also possible to use conditional formatting and a little code to highlight a record.
I have a weird situation in Access. Normally, the Invalid use of Null error is quite a simple thing - assigning a null to a string variable or some such. But I'm getting the error in a place where it seems to me it should not be happening. Here is the code snippet:
bch = Form_Akces.txtMaxCisla.BackColor
If Err <> 0 Then Stop
Form_Akces.txtMaxCisla.BackColor = vbYellow
If Err <> 0 Then Stop
DoEvents
If Err <> 0 Then Stop ' This is where I get the error
With qdf_GPsp
What's been happening is that I get this error only sometimes, usually only on the first time I run the code in a while. If I close the database and immediately re-open it, usually I do not get the error. It's been driving me nuts for quite a while, so I put in all these "If Err <> 0 Then Stop" statements, trying to track down where it happens. It's a live system, and users know to simply restart the app, but it's a massive PIA, and kind of embarrassing to boot.
Can anybody think of something to try or examine? I'm not exactly an amateur in Access, but this is far outside anything I have ever run across. Why a DoEvents statement should generate such an error is beyond me, especially since I am not doing anything even in the preceeding statements that should generate such an error, that it might be somehow 'held' until the processor gets an opportunity to throw the error. If I take out the DoEvents, I simply get the same error somewhat further down the line. txtMaxCisla is an unbound text field on the form Form_Akces, from which the routine containing this code is called. It is only on start-up - once everything is loaded and running, this never happens again. And it only happens once in a while - no pattern to it that I have been able to detect.
It's been going on for a couple of months, through numerous compile, decompile, recompile, compress and repair cycles, with no discernible change except that sometimes it happens other places, again with no reason for it that I can see.
Update *
No luck - it still crashes, and for absolutely NO reason that I can see. Here's the code now:
Public Sub ReloadMaxNumbers(tmc As TextBox)
Dim rst As DAO.Recordset, x$, xb$, xe$, bch&
On Error GoTo 0
If Err <> 0 Then Stop
DoEvents
If Err <> 0 Then Stop
...
The code stops on the SECOND test, after the DoEvents, with the same error, "Invalid use of Null". I realize this code is completely retarded, but it's the result of tracking back, trying to find the root of the error. Without this, it crashes further down the road somewhere, with this same error. At this point I can't think of anything else to even try.
I'm puzzled by Form_Akces in your code. If I create a form named Akces, the form's code module is named Form_Akces. But you said "on the form Form_Akces". So I am confused whether Form_Akces is the name of the form or the form's code module. Perhaps Access is also confused.
Either way, since you said the code is in the form's code module, I suggest you substitute Me for Form_Akces
Edit: I misunderstood your situation. That code you showed us is actually from a procedure in another code module, not the form's code module. In that case I would do something like this for the external procedure DoSomething:
Public Sub DoSomething(ByRef frm As Form)
bch = frm.txtMaxCisla.BackColor
frm.txtMaxCisla.BackColor = vbYellow
DoEvents
' whatever else you need
End Sub
Then in the form's code module where you call DoSomething:
DoSomething Me
And if DoSomething only operates on a single control each time, you could just pass a reference to that control instead.
Public Sub DoSomething(ByRef ctl As Control)
This approach will allow DoSomething to be re-used for other forms with no changes required because the target form name is not "hard-wired" into the procedure. Also, it won't break if you rename your Akces form. In the second variation, it would also accommodate changes to the control name.
I am writing a VBA function to import data from one table to another in Access. The table I'm importing into has more strict data constraints (i.e. types, size etc.), so I'm expecting a lot of errors.
Rather than sift through every VBA error that comes up, I want my recordset loop to skip the entire current record and make a note of it in a separate table whenever it runs into an error. So every other line I've inserted On Error GoTo RecordError. But for some reason it's not handling every error. My code just breaks and tells me what the error is. I have the "Break on Unhandled Exceptions" option checked already.
Here's a screenshot that should explain it.
Why would it be breaking on the line immediately following an Error handler?
I think you're not understanding how VB(A) error handling works. Follow these principles:
An On Error... statement only applies to the routine (Sub or Function) in which it appears (though it will also catch errors that "bubble up" from routines that are called from within the routine in which you use it).
On Error sets a state. That is, Once you issue an On Error... it remains in force for the rest of the routine, unless superceded by a new On Error....
There are four forms of On Error...:
On Error GoTo <label>: <label> must be defined in the same routine, by writing the label name immediately followed by a colon (:) on a line by itself.
On Error Resume: immediately retries the error-throwing statement. Hardly ever used, since it's potentially infinite.
On Error Resume Next: ignores the error & continues. Sometimes useful at the end of routines for cleanup (for instance, if you want to Close a Recordset that may or may not be open). Alternatively, this form can also be used if you check the Err object immediately after any potentially error-throwing line (if Err.Number is zero (0), the statement succeeded without throwing an error). This is way too much work for most situations.
On Error GoTo 0: turns off error handling.
Given this, it's usual to place the On Error... statement immediately followng the routine's declaration (the Sub or Function statement), though some people put their Dim statements in between. If you want to temporarily change the manner of error handling within a routine, put the "new" one right before the code to which it is to apply, and (if used), the "revert" (reissuing the original), right after.
Even given all that, I have no idea why it would break on the error-throwing line when "Break on Unhandled Errors" is selected, unless you've managed to confuse it so much that it thinks there's no active error handling (and I'd be surprised if it compiled if that were the case).
Note that David Heffernan gave you the essential part of this in his answer, and it was here before mine....
The reason it is not working is because you cannot use On Error Goto ... within an error handler.
see http://www.cpearson.com/excel/errorhandling.htm
you cannot use On Error to skip a few lines, instead on error should go to a error handler which then resume's to the desired next line (in your example you could probably get away with one error handler which contains a resume next which will take you back to the next field).
thanks to Tim Williams on this question: The second of 2 'On Error goto ' statements gets ignored
and BTW ParseInt on a ZIP will destroy zip codes that begin with a 0, zipcodes should probably be treated as text.
You need to place the On Error line before the code whose errors you wish to handle.
What's more you only need to have one On Error line. The error handler then stays active until the subroutine exits or you execute another On Error statement.
Error handling with VBA is a real PITA. I'd propose you to have a look at this answer to the 'MS-Access, VBA and error handling' question, and have it adapted to your own situation. You can easily write down some code that will store all your error messages in a table, building a de facto error reporting system.
Setting the debug mode to 'break on all errors' will make the program execution stop at the line that causes an error even when the error handler has been correctly written. This can be confusing as it appears that error handling is not working.
Nobody has really answered your question.
Say your code is something like this (a skeletal framework):
Public Sub MySub()
On Error GoTo errHandler
Dim rs As DAO.Recordset
Set rs = CurrentDB.OpenRecords([SQL SELECT])
If rs.RecordCount >0 Then
rs.MoveFirst
Do Until rs.EOF
[do whatever that produces the error]
errSkipToNext:
rs.MoveNext
Loop
End If
exitRoutine:
If Not (rs Is Nothing) Then
rs.Close
Set rs = Nothing
Exit Sub
errHandler:
Select Case Err.Number
Case X, Y, Z ' where these are error numbers you want to ignore
Err.Clear
' do whatever it is you need to do in order to record the offending row
Call RecordError(rs!PK, Err.Number) ' PK is a field that identifies the bad record
GoTo errSkipToNext
Case Else
MsgBox Err.Number & ": " & Err.Description, vbExclamation, _
"Error!"
Resume exitRoutine
End Select
End Sub
In this code, you use a SELECT CASE in your error handler to decide which errors you want to ignore. In my code framework above, I listed the error numbers as X, Y, Z, but you'd replace that with the real error numbers you want to ignore, instead.
You don't want to ignore every single error because you might end up ignoring important errors elsewhere in your subroutine. If you don't want to figure out what the limited number of errors you want to ignore happen to be, I would suggest that you set a flag at the beginning of the code block that produces the errors you want to ignore, then use an `If bolErrorInCodeBlockToIgnore Then to decide if you're ignoring all errors or not. Something like this:
Public Sub MySub()
On Error GoTo errHandler
Dim rs As DAO.Recordset
Dim bolErrorInCodeBlockToIgnore As Boolean
Set rs = CurrentDB.OpenRecords([SQL SELECT])
If rs.RecordCount >0 Then
rs.MoveFirst
Do Until rs.EOF
bolErrorInCodeBlockToIgnore = True
[do whatever that produces the error]
errSkipToNext:
rs.MoveNext
Loop
End If
exitRoutine:
If Not (rs Is Nothing) Then
rs.Close
Set rs = Nothing
Exit Sub
errHandler:
If bolErrorInCodeBlockToIgnore Then
Err.Clear
' do whatever it is you need to do in order to record the offending row
Call RecordError(rs!PK, Err.Number) ' PK is a field that identifies the bad record
bolErrorInCodeBlockToIgnore = False
GoTo errSkipToNext
Else
MsgBox Err.Number & ": " & Err.Description, vbExclamation, _
"Error!"
Resume exitRoutine
End If
End Sub
I would much prefer the first, as I'm a firm believer in only ignoring known errors, not any old error that happens. But it might be quite difficult to come up with tests that will produce all the possible errors you want to ignore.
I have seen error handling fail too. Here is one example.
Public Function Have(ByVal item As Variant) As Boolean
'Have = Have data. Simplifies handling nulls and empty strings in validation code
On Error GoTo Procerr
If IsNull(item) Then
Have = False
**ElseIf Len(Trim(item)) = 0 Then 'Faster than Item <> ""**
Have = False
ElseIf item = 0 Then
Have = False
Else
Have = True
End If
exitproc:
Exit Function
Procerr:
'Errors sometimes occur if an unbound control is referenced
Have = False
End Function
The code sometimes fails on the line flagged with **. Here is the error message.
Note that the error handler has failed. In this case, the form that called the code returned had its recordsource set on the fly to an empty recordset, hence the fields on the screen are not visible. The form is a continuous form, so records and fields are not visible when the form is loaded with an empty recordset. The have() function is not directly called by my code, but somehow seems to be triggered by the me.requery method. The have() has been called hundreds of millions of times in my code but this is the only instance that causes it to fail and the error handler is not involked.
To Lance Roberts re original question. utf-8 unicode can sometimes play havoc with ms-access as it seems to be allow data to be confused for instruction codes (my guess). utf-8 can get into your data if data was originally loaded from a text file. utf-8 with a byte order mark (BoM) is particularly nasty. When you run some procedure that works with the data, strange errors can occur and it may look like your file has become corrupt. In other cases, text handling functions give wrong answers, e.g. Mid() will see the BOM and if you specify a starting point will start at the BOM, but Len() ignores the BOM. I am speculating that if you have this issue, then ms-access may not handle errors correctly. I have had similar issues importing data and importing utf-8 as ANSI was the cause. Note that utf-8 and ANSI are identical most of the time for plain English data so your errors may not be on every line. My errors were mostly with time-date fields. Try exporting the data first and then forcing it to be ANSI and remove any BoM and and reimporting it.