Quels sont les bons modèles pour la gestion des erreurs dans VBA?
En particulier, que dois-je faire dans cette situation:
... some code ...
... some code where an error might occur ...
... some code ...
... some other code where a different error might occur ...
... some other code ...
... some code that must always be run (like a finally block) ...
Je veux gérer les deux erreurs et reprendre l'exécution après le code où l'erreur peut se produire. En outre, le code finally à la fin doit TOUJOURS être exécuté, quelles que soient les exceptions lancées précédemment. Comment puis-je atteindre ce résultat?
On Error Goto
ErrorHandlerLabel Resume
(Next
| ErrorHandlerLabel )On Error Goto 0
_ (désactive le gestionnaire d’erreurs actuel)Err
objetLes propriétés de l'objet Err
sont normalement réinitialisées à zéro ou à une chaîne de longueur nulle dans la routine de traitement des erreurs, mais cela peut également être fait explicitement avec Err.Clear
.
Les erreurs dans la routine de traitement des erreurs se terminent.
La plage 513-65535 est disponible pour les erreurs utilisateur. Pour les erreurs de classe personnalisées, vous ajoutez vbObjectError
au numéro d'erreur. Voir la documentation MS sur Err.Raise
et le liste des numéros d'erreur .
Pour les membres d'interface non implémentés dans une classe dérivée , vous devez utiliser la constante E_NOTIMPL = &H80004001
.
Option Explicit
Sub HandleError()
Dim a As Integer
On Error GoTo errMyErrorHandler
a = 7 / 0
On Error GoTo 0
Debug.Print "This line won't be executed."
DoCleanUp:
a = 0
Exit Sub
errMyErrorHandler:
MsgBox Err.Description, _
vbExclamation + vbOKCancel, _
"Error: " & CStr(Err.Number)
Resume DoCleanUp
End Sub
Sub RaiseAndHandleError()
On Error GoTo errMyErrorHandler
' The range 513-65535 is available for user errors.
' For class errors, you add vbObjectError to the error number.
Err.Raise vbObjectError + 513, "Module1::Test()", "My custom error."
On Error GoTo 0
Debug.Print "This line will be executed."
Exit Sub
errMyErrorHandler:
MsgBox Err.Description, _
vbExclamation + vbOKCancel, _
"Error: " & CStr(Err.Number)
Err.Clear
Resume Next
End Sub
Sub FailInErrorHandler()
Dim a As Integer
On Error GoTo errMyErrorHandler
a = 7 / 0
On Error GoTo 0
Debug.Print "This line won't be executed."
DoCleanUp:
a = 0
Exit Sub
errMyErrorHandler:
a = 7 / 0 ' <== Terminating error!
MsgBox Err.Description, _
vbExclamation + vbOKCancel, _
"Error: " & CStr(Err.Number)
Resume DoCleanUp
End Sub
Sub DontDoThis()
' Any error will go unnoticed!
On Error Resume Next
' Some complex code that fails here.
End Sub
Sub DoThisIfYouMust()
On Error Resume Next
' Some code that can fail but you don't care.
On Error GoTo 0
' More code here
End Sub
J'ajouterais aussi:
Err
est le plus proche d’un objet exception.Err.Raise
Et juste pour le plaisir:
On Error Resume Next
est le diable incarné et à éviter, car il cache silencieusement les erreursPour que tu puisses faire quelque chose comme ça
Function Errorthingy(pParam)
On Error GoTo HandleErr
' your code here
ExitHere:
' your finally code
Exit Function
HandleErr:
Select Case Err.Number
' different error handling here'
Case Else
MsgBox "Error " & Err.Number & ": " & Err.Description, vbCritical, "ErrorThingy"
End Select
Resume ExitHere
End Function
Si vous souhaitez utiliser des exceptions personnalisées. (par exemple, ceux qui enfreignent les règles de gestion), utilisez l'exemple ci-dessus, mais utilisez la commande goto pour modifier le flux de la méthode si nécessaire.
Voici mon implémentation standard. J'aime que les étiquettes soient auto-descriptives.
Public Sub DoSomething()
On Error GoTo Catch ' Try
' normal code here
Exit Sub
Catch:
'error code: you can get the specific error by checking Err.Number
End Sub
Ou, avec un bloc Finally
:
Public Sub DoSomething()
On Error GoTo Catch ' Try
' normal code here
GoTo Finally
Catch:
'error code
Finally:
'cleanup code
End Sub
Développement professionnel Excel a un très bon schéma de traitement des erreurs . Si vous allez passer du temps en VBA, cela vaut probablement la peine de vous procurer le livre. VBA manque dans un certain nombre de domaines et ce livre propose de bonnes suggestions pour la gestion de ces domaines.
PED décrit deux méthodes de traitement des erreurs. Le principal est un système où toutes les procédures de point d’entrée sont des sous-procédures et toutes les autres procédures sont des fonctions qui retournent des booléens.
La procédure de point d’entrée utilise les instructions On Error pour capturer les erreurs à peu près telles qu’elles ont été conçues. Les procédures de point de non-entrée renvoient True s'il n'y a pas d'erreur et False s'il y en a. Les procédures de point non-entrée utilisent également On Error.
Les deux types de procédures utilisent une procédure centrale de traitement des erreurs pour conserver l'erreur et la consigner.
Le code ci-dessous montre une alternative qui garantit qu'il n'y a qu'un seul point de sortie pour la sous/fonction.
sub something()
on error goto errHandler
' start of code
....
....
'end of code
' 1. not needed but signals to any other developer that looks at this
' code that you are skipping over the error handler...
' see point 1...
err.clear
errHandler:
if err.number <> 0 then
' error handling code
end if
end sub
J'utilise un morceau de code que j'ai développé moi-même et c'est très bon pour mes codes:
Au début de la fonction ou sous, je définis:
On error Goto ErrorCatcher:
et puis, je gère les erreurs possibles
ErrorCatcher:
Select Case Err.Number
Case 0 'exit the code when no error was raised
On Error GoTo 0
Exit Function
Case 1 'Error on definition of object
'do stuff
Case... 'little description here
'do stuff
Case Else
Debug.Print "###ERROR"
Debug.Print " • Number :", Err.Number
Debug.Print " • Descrip :", Err.Description
Debug.Print " • Source :", Err.Source
Debug.Print " • HelpCont:", Err.HelpContext
Debug.Print " • LastDLL :", Err.LastDllError
Stop
Err.Clear
Resume
End Select
La fonction relativement inconnue Erl
est également pertinente pour la discussion. Si vous avez des étiquettes numériques dans votre procédure de code, par exemple,
Sub AAA()
On Error Goto ErrorHandler
1000:
' code
1100:
' more code
1200:
' even more code that causes an error
1300:
' yet more code
9999: ' end of main part of procedure
ErrorHandler:
If Err.Number <> 0 Then
Debug.Print "Error: " + CStr(Err.Number), Err.Descrption, _
"Last Successful Line: " + CStr(Erl)
End If
End Sub
La fonction Erl
renvoie le dernier libellé de ligne numéroté. Dans l'exemple ci-dessus, si une erreur d'exécution se produit après label 1200:
mais avant 1300:
, la fonction Erl
retournera 1200
, puisque c’est l’étiquette de ligne la plus récemment rencontrée. Je trouve bon de placer une étiquette de ligne immédiatement au-dessus de votre bloc de traitement des erreurs. J'utilise typiquement 9999
pour indiquer que la partie principale de la procédure a abouti à sa conculsion attendue.
REMARQUES:
Les étiquettes de ligne DOIVENT être des entiers positifs - une étiquette comme MadeItHere:
_ n'est pas reconnu par Erl
.
Les libellés de ligne n'ont aucun rapport avec les numéros de ligne réels d'un VBIDE CodeModule
. Vous pouvez utiliser tous les nombres positifs que vous voulez, dans l'ordre de votre choix. Dans l'exemple ci-dessus, il n'y a qu'une vingtaine de lignes de code, mais les numéros d'étiquette de ligne commencent par 1000
. Il n'y a pas de relation entre les numéros de ligne de l'éditeur et les numéros d'étiquette de ligne utilisés avec Erl
.
Les numéros d’étiquette de ligne ne doivent pas nécessairement figurer dans un ordre particulier. Toutefois, s’ils ne sont pas en ordre croissant, l’efficacité et les avantages de Erl
sont considérablement diminués, mais Erl
indique toujours le numéro correct.
Les étiquettes de ligne sont spécifiques à la procédure dans laquelle elles apparaissent. Si la procédure ProcA
appelle la procédure ProcB
et qu’une erreur se produit dans ProcB
, elle renvoie le contrôle à ProcA
, Erl
(dans ProcA
) renverra le dernier numéro d'étiquette de ligne de rencontred dans ProcA
avant d'appeler ProcB
. Dans ProcA
, vous ne pouvez pas obtenir les numéros d'étiquette de ligne pouvant apparaître dans ProcB
.
Faites attention lorsque vous insérez des étiquettes de numéro de ligne dans une boucle. Par exemple,
For X = 1 To 100
500:
' some code that causes an error
600:
Next X
Si le code suivant l’étiquette de ligne 500
mais avant 600
provoque une erreur, et cette erreur survient à la 20ème itération de la boucle, Erl
retournera 500
, même si 600
a été rencontré avec succès lors des 19 dernières interactions de la boucle.
Le placement correct des étiquettes de ligne dans la procédure est essentiel pour utiliser la fonction Erl
afin d’obtenir des informations vraiment utiles.
Il existe un grand nombre d'utilisations gratuites sur le réseau qui insèrent automatiquement une étiquette de ligne numérique dans une procédure. Vous disposez ainsi d'informations d'erreur détaillées lors du développement et du débogage, puis supprimez ces étiquettes une fois le code activé.
Si votre code affiche des informations d'erreur à l'utilisateur final si une erreur inattendue se produit, fournir la valeur de Erl
dans cette information peut rendre la recherche et la résolution du problème VASTLY plus simples que si la valeur de Erl
ne l'est pas signalé.
Voici un modèle assez décent.
Pour le débogage: quand une erreur est levée, appuyez sur Ctrl-Pause (ou Ctrl-Pause), faites glisser le marqueur de rupture (ou le nom de votre choix) vers la ligne de reprise, appuyez sur F8 et vous passerez à la ligne qui "jeta" l'erreur.
ExitHandler est votre "Enfin".
Le sablier sera tué à chaque fois. Le texte de la barre d'état sera effacé à chaque fois.
Public Sub ErrorHandlerExample()
Dim dbs As DAO.Database
Dim rst As DAO.Recordset
On Error GoTo ErrHandler
Dim varRetVal As Variant
Set dbs = CurrentDb
Set rst = dbs.OpenRecordset("SomeTable", dbOpenDynaset, dbSeeChanges + dbFailOnError)
Call DoCmd.Hourglass(True)
'Do something with the RecordSet and close it.
Call DoCmd.Hourglass(False)
ExitHandler:
Set rst = Nothing
Set dbs = Nothing
Exit Sub
ErrHandler:
Call DoCmd.Hourglass(False)
Call DoCmd.SetWarnings(True)
varRetVal = SysCmd(acSysCmdClearStatus)
Dim errX As DAO.Error
If Errors.Count > 1 Then
For Each errX In DAO.Errors
MsgBox "ODBC Error " & errX.Number & vbCrLf & errX.Description
Next errX
Else
MsgBox "VBA Error " & Err.Number & ": " & vbCrLf & Err.Description & vbCrLf & "In: Form_MainForm", vbCritical
End If
Resume ExitHandler
Resume
End Sub
Select Case Err.Number
Case 3326 'This Recordset is not updateable
'Do something about it. Or not...
Case Else
MsgBox "VBA Error " & Err.Number & ": " & vbCrLf & Err.Description & vbCrLf & "In: Form_MainForm", vbCritical
End Select
Il intercepte également les erreurs DAO et VBA. Vous pouvez placer un cas de sélection dans la section d'erreur VBA si vous souhaitez intercepter des numéros Err spécifiques.
Select Case Err.Number
Case 3326 'This Recordset is not updateable
'Do something about it. Or not...
Case Else
MsgBox "VBA Error " & Err.Number & ": " & vbCrLf & Err.Description & vbCrLf & "In: Form_MainForm", vbCritical
End Select
Je trouve que les solutions suivantes fonctionnent le mieux, appelée approche de traitement des erreurs centralisée.
Vous avez 2 modes d'exécution de votre application: Debug et Production. Dans le mode Debug, le code s’arrêtera à toute erreur imprévue et vous permettra de déboguer facilement en sautant sur la ligne où elle s’est produite en appuyant deux fois sur F8. En mode Production, un message d'erreur significatif sera affiché à l'utilisateur.
Vous pouvez lancer des erreurs intentionnelles comme celle-ci, qui arrêteront l'exécution du code avec un message à l'utilisateur:
Err.Raise vbObjectError, gsNO_DEBUG, "Some meaningful error message to the user"
Err.Raise vbObjectError, gsUSER_MESSAGE, "Some meaningful non-error message to the user"
'Or to exit in the middle of a call stack without a message:
Err.Raise vbObjectError, gsSILENT
Vous devez "encapsuler" tous les sous-programmes et fonctions avec une quantité importante de code avec les en-têtes et les pieds de page suivants, en veillant à spécifier ehCallTypeEntryPoint
dans tous vos points d'entrée. Notez également la constante msModule
qui doit être mise dans tous les modules.
Option Explicit
Const msModule As String = "<Your Module Name>"
' This is an entry point
Public Sub AnEntryPoint()
Const sSOURCE As String = "AnEntryPoint"
On Error GoTo ErrorHandler
'Your code
ErrorExit:
Exit Sub
ErrorHandler:
If CentralErrorHandler(Err, ThisWorkbook, msModule, sSOURCE, ehCallTypeEntryPoint) Then
Stop
Resume
Else
Resume ErrorExit
End If
End Sub
' This is any other subroutine or function that isn't an entry point
Sub AnyOtherSub()
Const sSOURCE As String = "AnyOtherSub"
On Error GoTo ErrorHandler
'Your code
ErrorExit:
Exit Sub
ErrorHandler:
If CentralErrorHandler(Err, ThisWorkbook, msModule, sSOURCE) Then
Stop
Resume
Else
Resume ErrorExit
End If
End Sub
Le contenu du module central de gestion des erreurs est le suivant:
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Comments: Error handler code.
'
' Run SetDebugMode True to use debug mode (Dev mode)
' It will be False by default (Production mode)
'
' Author: Igor Popov
' Date: 13 Feb 2014
' Licence: MIT
'
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Option Explicit
Option Private Module
Private Const msModule As String = "MErrorHandler"
Public Const gsAPP_NAME As String = "<You Application Name>"
Public Const gsSILENT As String = "UserCancel" 'A silent error is when the user aborts an action, no message should be displayed
Public Const gsNO_DEBUG As String = "NoDebug" 'This type of error will display a specific message to the user in situation of an expected (provided-for) error.
Public Const gsUSER_MESSAGE As String = "UserMessage" 'Use this type of error to display an information message to the user
Private Const msDEBUG_MODE_COMPANY = "<Your Company>"
Private Const msDEBUG_MODE_SECTION = "<Your Team>"
Private Const msDEBUG_MODE_VALUE = "DEBUG_MODE"
Public Enum ECallType
ehCallTypeRegular = 0
ehCallTypeEntryPoint
End Enum
Public Function DebugMode() As Boolean
DebugMode = CBool(GetSetting(msDEBUG_MODE_COMPANY, msDEBUG_MODE_SECTION, msDEBUG_MODE_VALUE, 0))
End Function
Public Sub SetDebugMode(Optional bMode As Boolean = True)
SaveSetting msDEBUG_MODE_COMPANY, msDEBUG_MODE_SECTION, msDEBUG_MODE_VALUE, IIf(bMode, 1, 0)
End Sub
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Comments: The central error handler for all functions
' Displays errors to the user at the entry point level, or, if we're below the entry point, rethrows it upwards until the entry point is reached
'
' Returns True to stop and debug unexpected errors in debug mode.
'
' The function can be enhanced to log errors.
'
' Date Developer TDID Comment
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' 13 Feb 2014 Igor Popov Created
Public Function CentralErrorHandler(ErrObj As ErrObject, Wbk As Workbook, ByVal sModule As String, ByVal sSOURCE As String, _
Optional enCallType As ECallType = ehCallTypeRegular, Optional ByVal bRethrowError As Boolean = True) As Boolean
Static ssModule As String, ssSource As String
If Len(ssModule) = 0 And Len(ssSource) = 0 Then
'Remember the module and the source of the first call to CentralErrorHandler
ssModule = sModule
ssSource = sSOURCE
End If
CentralErrorHandler = DebugMode And ErrObj.Source <> gsNO_DEBUG And ErrObj.Source <> gsUSER_MESSAGE And ErrObj.Source <> gsSILENT
If CentralErrorHandler Then
'If it's an unexpected error and we're going to stop in the debug mode, just write the error message to the immediate window for debugging
Debug.Print "#Err: " & Err.Description
ElseIf enCallType = ehCallTypeEntryPoint Then
'If we have reached the entry point and it's not a silent error, display the message to the user in an error box
If ErrObj.Source <> gsSILENT Then
Dim sMsg As String: sMsg = ErrObj.Description
If ErrObj.Source <> gsNO_DEBUG And ErrObj.Source <> gsUSER_MESSAGE Then sMsg = "Unexpected VBA error in workbook '" & Wbk.Name & "', module '" & ssModule & "', call '" & ssSource & "':" & vbCrLf & vbCrLf & sMsg
MsgBox sMsg, vbOKOnly + IIf(ErrObj.Source = gsUSER_MESSAGE, vbInformation, vbCritical), gsAPP_NAME
End If
ElseIf bRethrowError Then
'Rethrow the error to the next level up if bRethrowError is True (by Default).
'Otherwise, do nothing as the calling function must be having special logic for handling errors.
Err.Raise ErrObj.Number, ErrObj.Source, ErrObj.Description
End If
End Function
Pour vous définir en mode Debug, exécutez ce qui suit dans la fenêtre Immediate:
SetDebugMode True
Mon point de vue personnel sur une déclaration faite précédemment dans ce fil de discussion:
Et juste pour le plaisir:
On Error Resume Next est le diable incarné et à éviter car il cache les erreurs en silence.
J'utilise le On Error Resume Next
_ sur des procédures pour lesquelles je ne souhaite pas qu'une erreur arrête mon travail et où une déclaration ne dépende pas du résultat des déclarations précédentes.
Quand je le fais, j'ajoute une variable globale debugModeOn
et la fixe à True
. Ensuite, je l'utilise de cette façon:
If not debugModeOn Then On Error Resume Next
Lorsque je transmets mon travail, je mets la variable à false, masquant ainsi les erreurs uniquement à l'utilisateur et les affichant lors des tests.
Vous pouvez également l'utiliser lorsque vous rencontrez un problème qui pourrait échouer, par exemple appeler le DataBodyRange d'un ListObject pouvant être vide:
On Error Resume Next
Sheet1.ListObjects(1).DataBodyRange.Delete
On Error Goto 0
Au lieu de:
If Sheet1.ListObjects(1).ListRows.Count > 0 Then
Sheet1.ListObjects(1).DataBodyRange.Delete
End If
Ou vérifier l'existence d'un article dans une collection:
On Error Resume Next
Err.Clear
Set auxiliarVar = collection(key)
' Check existence (if you try to retrieve a nonexistant key you get error number 5)
exists = (Err.Number <> 5)