VBA错误处理的好模式

时间:2009-06-24 12:17:16

标签: exception vba exception-handling

VBA中的错误处理有哪些好的模式?

特别是在这种情况下我应该怎么做:

... 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) ...

我想处理这两个错误,并在可能发生错误的代码之后继续执行。此外,最后的代码必须始终运行 - 无论先前抛出什么异常。我怎样才能实现这一结果?

12 个答案:

答案 0 :(得分:96)

VBA中的错误处理


  • On Error Goto ErrorHandlerLabel
  • ResumeNext | ErrorHandlerLabel
  • On Error Goto 0(禁用当前错误处理程序)
  • Err object

Err对象的属性通常在错误处理例程中重置为零或零长度字符串,但也可以使用Err.Clear显式完成。

错误处理例程中的错误正在终止。

范围513-65535可用于用户错误。 对于自定义类错误,请将vbObjectError添加到错误编号中。 请参阅有关Err.Raiselist of error numbers

的MS文档

对于派生类中未实现的接口成员,您应该使用常量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

答案 1 :(得分:34)

我还想补充一下:

  • 全局Err对象是您与异常对象最接近的对象
  • 您可以使用Err.Raise
  • 有效地“抛出异常”

只是为了好玩:

  • On Error Resume Next是魔鬼的化身并且要避免,因为它默默地隐藏错误

答案 2 :(得分:16)

所以你可以做这样的事情

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

如果您想烘焙自定义例外。 (例如违反业务规则的那些)使用上面的例子,但是根据需要使用goto来改变方法的流程。

答案 3 :(得分:11)

这是我的标准实施。我喜欢标签是自我描述的。

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

或者,使用Finally块:

Public Sub DoSomething()

    On Error GoTo Catch ' Try

    ' normal code here

    GoTo Finally
Catch:

    'error code

Finally:

    'cleanup code

End Sub

答案 4 :(得分:4)

Professional Excel Development非常好error handling scheme。如果您打算在VBA中度过任何时间,那么这本书可能值得一试。 VBA缺乏许多领域,本书对管理这些领域提出了很好的建议。

PED描述了两种错误处理方法。主要的一个系统是所有入口点程序都是子程序,所有其他程序都是返回布尔值的函数。

入口点过程使用On Error语句来捕获与设计相当的错误。如果没有错误,非入口点程序返回True,如果有错误则返回False。非入口点程序也使用On Error。

两种类型的过程都使用中央错误处理过程来保持错误状态并记录错误。

答案 5 :(得分:3)

我使用了自己开发的一段代码,这对我的代码非常有用:

在函数或子函数的开头,我定义:

On error Goto ErrorCatcher:

然后,我处理可能的错误

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

答案 6 :(得分:3)

这是一个相当不错的模式。

用于调试:当出现错误时,按Ctrl-Break(或Ctrl-Pause),将中断标记(或其任何调用的内容)向下拖动到Resume行,点击F8然后你将走到那一行“扔了”错误。

ExitHandler是你的“终极”。

每次都会杀死沙漏。 每次都会清除状态栏文本。

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

它还会捕获DAO和VBA错误。如果要捕获特定的Err编号,可以在VBA错误部分中放置一个Select Case。

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

答案 7 :(得分:3)

下面的代码显示了一个替代方案,确保子/函数只有一个退出点。

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

答案 8 :(得分:3)

与讨论相关的还有相对未知的Erl函数。如果您的代码过程中有数字标签,例如

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 

Erl函数返回最近遇到的数字线标签。在上面的示例中,如果在标记1200:之后但1300:之前发生运行时错误,则Erl函数将返回1200,因为这是最成熟的成功遇到的行标签。我发现在错误处理块的正上方放置一个行标签是一个好习惯。我通常使用9999表示该程序的主要部分达到预期的强制性。

注意:

  • 行标签必须是正整数 - MadeItHere:之类的标签不会被Erl重新识别。

  • 行标签与VBIDE CodeModule的实际行号完全无关。您可以按照您想要的任何顺序使用您想要的任何正数。在上面的示例中,只有大约25行代码,但行标签号从1000开始。编辑器行号与Erl使用的行标签号之间没有关系。

  • 行标签号不需要按任何特定顺序排列,但如果它们不是按升序排列,那么Erl的功效和效益会大大降低,但是Erl仍会报告正确的号码。

  • 行标签特定于它们出现的过程。如果过程ProcA调用过程ProcB并且ProcB中发生错误,将控制权传回ProcA,则Erl(在ProcA中)将返回在ProcA调用ProcB之前,ProcA中最近遇到的行标签号。在ProcB内,您无法获得For X = 1 To 100 500: ' some code that causes an error 600: Next X 中可能出现的行标签数字。

将行号标签放入循环中时要小心。例如,

500

如果行标签600之后但Erl之前的代码导致错误,并且在循环的第20次迭代中出现该错误,则500将返回600,即使Erl在循环的前19个迭代中已成功遇到。

在程序中正确放置行标签对于使用Erl函数获取真正有意义的信息至关重要。

网上有任意数量的免费实用程序会自动在程序中插入数字行标签,因此在开发和调试时会有细粒度的错误信息,然后在代码生效后删除这些标签。

如果您的代码在发生意外错误时向最终用户显示错误信息,那么从Erl中提供该信息中的值可以比{{1}}的值更简单地查找和修复问题没有报道。

答案 9 :(得分:2)

小心大象陷阱:

我在这次讨论中没有提到这一点。 [Access 2010]

ACCESS / VBA如何处理CLASS对象中的错误由可配置选项决定:

VBA代码编辑器&gt;工具&gt;选项&gt;一般&gt;错误捕获:

enter image description here

答案 10 :(得分:2)

我发现以下方法效果最好,称为中央错误处理方法。

优势

您有两种运行应用程序的模式:调试生产。在 Debug 模式下,代码将在任何意外错误时停止,并允许您通过按两次F8跳转到它出现的行来轻松调试。在 Production 模式下,将向用户显示有意义的错误消息。

您可以抛出这样的故意错误,这将停止执行代码并向用户发送消息:

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

实施

您需要使用以下页眉和页脚“包装”所有子程序和函数,并确保在所有入口点中指定ehCallTypeEntryPoint。另请注意msModule常量,需要将其放在所有模块中。

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

中央错误处理程序模块的内容如下:

'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' 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

要将自己设置为 Debug 模式,请在立即窗口中运行以下命令:

SetDebugMode True

答案 11 :(得分:1)

我个人对此主题前面发表的声明的看法是:

  

只是为了好玩:

     

On Error Resume Next是魔鬼的化身并且要避免,因为它会默默地隐藏错误。

我正在使用On Error Resume Next程序,我不想让错误停止我的工作,以及任何语句不依赖于前面语句的结果。

当我这样做时,我添加了一个全局变量debugModeOn,并将其设置为True。然后我这样用它:

If not debugModeOn Then On Error Resume Next

当我交付我的工作时,我将变量设置为false,从而仅将错误隐藏给用户并在测试期间显示它们。

在执行可能失败的操作时也使用它,例如调用可能为空的ListObject的DataBodyRange:

On Error Resume Next
Sheet1.ListObjects(1).DataBodyRange.Delete
On Error Goto 0

而不是:

If Sheet1.ListObjects(1).ListRows.Count > 0 Then 
    Sheet1.ListObjects(1).DataBodyRange.Delete
End If

或检查集合中是否存在项目:

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)