Вопрос по exception, vba – Хорошие шаблоны для обработки ошибок VBA

69

Каковы хорошие шаблоны для обработки ошибок в VBA?

В частности, что мне делать в этой ситуации:

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

Я хочу обработать обе ошибки и возобновить выполнение после кода, в котором может произойти ошибка. Кроме того, код finally в конце должен ВСЕГДА выполняться - независимо от того, какие исключения были сгенерированы ранее. Как я могу достичь этого результата?

Ваш Ответ

12   ответов
4

Профессиональная разработка в Excel имеет довольно хорошийсхема обработки ошибок, Если вы собираетесь провести какое-то время в VBA, возможно, стоит приобрести книгу. Есть ряд областей, где VBA не хватает, и в этой книге есть хорошие советы по управлению этими областями.

PED описывает два метода обработки ошибок. Основной является система, в которой все процедуры точки входа являются подпроцедурами, а все остальные процедуры являются функциями, которые возвращают логические значения.

Процедура точки входа использует операторы On Error, чтобы фиксировать ошибки почти так же, как было задумано. Процедуры без точки входа возвращают True, если ошибок не было, и False, если были ошибки. Процедуры без точки входа также используют On Error.

Оба типа процедур используют центральную процедуру обработки ошибок, чтобы сохранить ошибку в ее состоянии и зарегистрировать ошибку.

3

Для отладки: при возникновении ошибки нажмите Ctrl-Break (или Ctrl-Pause), перетащите маркер разрыва (или как он там называется) вниз на строку возобновления, нажмите F8, и вы перейдете на линию, которая " ; бросили & Quot; Ошибка.

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. Вы можете поместить Select Case в раздел ошибок VBA, если хотите перехватить определенные номера ошибок.

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
2

что лучше всего работает следующее, называемое центральным подходом к обработке ошибок.

Benefits

У вас есть 2 режима запуска приложения:Debug а такжеProduction, в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
Implementation

Вам нужно "обернуть" все подпрограммы и функции с любым значительным объемом кода со следующими верхними и нижними колонтитулами, обязательно указав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
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
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
1

And just for fun:

On Error Resume Next is the devil incarnate and to be avoided, as it silently hides errors.

Я используюOn Error Resume Next в процедурах, в которых я не хочу, чтобы из-за ошибки была остановлена моя работа, и где любое утверждение не зависит от результата предыдущих утверждений.

Когда я делаю это, я добавляю глобальную переменнуюdebugModeOn и я установил егоTrue, Тогда я использую это так:

If not debugModeOn Then On Error Resume Next

Когда я выполняю свою работу, я устанавливаю для переменной значение false, таким образом скрывая ошибки только для пользователя и показывая их во время тестирования.

Также используйте его, когда делаете что-то, что может не сработать, например, вызывая DataBodyRange объекта ListObject, который может быть пустым:

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)
& GT;If not debugModeOn Then On Error Resume Next В этом случае лучше использовать условную компиляцию вроде#If Hide_Errors > 0 Then On Error Resume Next и установитьHide_Errors в свойствах проекта VBAConditional Complication Arguments соответственно.
94
Error Handling in VBA


On Error Goto ErrorHandlerLabel Resume (Next | ErrorHandlerLabel) On Error Goto 0 (disables current error handler) Err object

Err свойства объекта обычно сбрасываются в ноль или строку нулевой длины в процедуре обработки ошибок, но это также может быть сделано явно сErr.Clear.

Ошибки в процедуре обработки ошибок заканчиваются.

Диапазон 513-65535 доступен для ошибок пользователя. Для пользовательских ошибок класса вы добавляетеvbObjectError на номер ошибки. Смотрите документацию MS оErr.Raise исписок номеров ошибок.

Для не реализованных членов интерфейса вderived класс, вы должны использовать константу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
это здорово, но есть ли место, где перечислены все ошибки, чтобы я мог знать, существует ли уже мой или нужно ли его создать?
@PsychoData, вот список кодов ошибокsupport.microsoft.com/kb/146864
Как код выше должен быть изменен, чтобы войти в систему иevery выход из вызываемого кода (процедура, функция, метод и т. д.)?
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
Что происходит, если исключение возникает послеFinally:? ТакOn Error GoTo 0 незамедлительно послеFinally: возможно, необходимо исправить нежелательную рекурсию.
@ AntoineL Да! Согласитесь с обоими, отличными наблюдениями и разъяснениями.
Если вы добавите ещеOn Error GoTo Catch2 вFinally: код, это будет эффективно в этом последнем случае, ноnot если вы прошлиCatch: раньше, потому что нетOn Error GoTo -1 ни какойResume; добавление первого приводит нас так далеко от обычногоtry catch finally что можно подумать о прекращении сомнительной аналогии до этого момента.
@LimaNightHawk: я верю, что произойдет послеFinally: зависит от того, введете ли вы его послеCatch: (тогда да просто выбрасывает) ... илиnot! И в этом последнем случае, то есть пройдяGoTo Finally будет иметьOn Error GoTo Catch все еще в силе, поэтому контроль перенаправлен наCatch: (может быть хорошо), тогдаFinally: повторно введен, вероятно, не то, что вы ожидали в первую очередь.
Если послеFinally блок, он просто выдаст ошибку. Он не будет возвращаться кFinally блок. (Попробуйте, вы увидите.) Если вы хотите обработать ошибку после блока Наконец, вам нужно будет добавить еще одинOn Error GoTo, но, вероятно, с другим лейблом, какCatch2, Но здесь мы начинаем углубляться в методологию чистого кода - & gt; чистый метод будет нуждаться только в одном обработчике ошибок (и даже должен иметь собственный метод для отлова ошибок).
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, чтобы при необходимости изменить поток метода.

Как правило, хорошей идеей вл етс "On Error GoTo 0". после блока кода, где вам нужна обработка ошибок. Кроме того, любая ошибка в коде обработки ошибок завершается.
Не знаю, если это идиоматический VBA, но для разработчиков .NET, если вы переименуете «HandleErr» «поймать» и & quot; ExitHere & quot; чтобы "Наконец-то" и косоглазие ...
@ user1454265 ... тогда вы можете легко пропуститьResume ExitHere что делает большую разницу между двумя парадигмами.
Именно так мы обрабатывали ошибки в больших приложениях VB6 в те времена. Работал относительно хорошо и был прост в использовании. IIRC, у нас был класс обработки ошибок, который вызывался вместо кода ошибки в функции. Таким образом, было намного проще изменить поведение.
34

The global Err object is the closest you have to an exception object You can effectively "throw an exception" with Err.Raise

И просто для удовольствия

On Error Resume Next is the devil incarnate and to be avoided, as it silently hides errors
+1 за предупреждение об Eror Resume Next. Вероятно, это одна из причин номер один, почему программы VB, как правило, так полны ошибок.
Я думаю, что все согласятся, что On Error - это эквивалент Try / Catch yes ... но On Error Resume Next? Это приводит к исчезновению всех ошибок, включая те, которые мы никогда не ожидали. Лучше позволить ошибкам в течение нескольких недель кровоточить, а не ломать голову над тем, почему происходит что-то странное [это случилось со мной при отладке чужого кода]. Я использую его только в очень особых обстоятельствах, в узких небольших функциях, где странность вынуждает вас ошибиться (например, существует ли этот элемент в Коллекции).
Не правда. При правильном использовании On Error Resume Next является эквивалентом try / catch. Правильное использование требует проверки или сохранения состояния ошибки послеevery line, Это делает сложную проверку ошибок намного менее многословной. ОДНАКО, неправильно используется, все вышесказанное применимо.
Если вы поместите слишком много кода в errMyErrorHandler: вы рискуете получить ошибку в вашем обработчике ошибок, который создает бесконечный цикл. Если вы установите On Error Resume Next до обработки ошибки в errMyErrorHandler, он сбрасывает объект Err, и вы теряете информацию об ошибке. Я перемещаю свою обработку ошибок в подпрограмму и передаю err.num и описание в качестве параметров, чтобы затем я мог использовать On Error Resume Next, так как я сбрасываю все, как screenupdating, курсор и т. Д., И показываю ошибку, используя значения параметров ...Call mdl_val.usr_sub_handle_error(Err.Source, Err.Description)
«Избегать» это не совсем так. Есть много случаев, требующихOn Error Resume Next, Общий принцип этих случаев заключается в том, что некоторые результаты возвращаются с помощью исключения. Наиболее частым случаем является доступ кCollection объект с помощью строкового ключа: в этом случае вызывающая сторона не может знать, есть ли элемент с этим ключом вCollection объект.
2

Beware the elephant trap:

Я не видел упоминаний об этом в этой дискуссии. [Доступ 2010]

Как ACCESS / VBA обрабатывает ошибки в объектах CLASS, определяется настраиваемым параметром:

Редактор кода VBA & gt; Инструменты & gt; Параметры & gt; Общие & gt; Перехват ошибок:

enter image description here

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 указать, что основная часть процедуры бежала до ожидаемого сотрясения.

ЗАМЕТКИ:

Line labels MUST be positive integers -- a label like MadeItHere: isn't recogonized by Erl.

Line labels are completely unrelated to the actual line numbers of a VBIDE CodeModule. You can use any positive numbers you want, in any order you want. In the example above, there are only 25 or so lines of code, but the line label numbers begin at 1000. There is no relationship between editor line numbers and line label numbers used with Erl.

Line label numbers need not be in any particular order, although if they are not in ascending, top-down order, the efficacy and benefit of Erl is greatly diminished, but Erl will still report the correct number.

Line labels are specific to the procedure in which they appear. If procedure ProcA calls procedure ProcB and an error occurs in ProcB that passes control back to ProcA, Erl (in ProcA) will return the most recently encounterd line label number in ProcA before it calls ProcB. From within ProcA, you cannot get the line label numbers that might appear in ProcB.

Соблюдайте осторожность при размещении меток номеров строк в цикле. Например,

For X = 1 To 100
500:
' some code that causes an error
600:
Next X

Если код после строки метки500 но прежде600 вызывает ошибку, и эта ошибка возникает на 20-й итерации цикла,Erl вернусь500, даже если600 был успешно встречен в предыдущих 19 взаимодействиях цикла.

Правильное размещение меток линий в рамках процедуры имеет решающее значение для использованияErl функция, чтобы получить действительно значимую информацию.

В сети имеется любое количество бесплатных утилит, которые автоматически вставляют в процедуру цифровую метку строки, поэтому вы получаете подробную информацию об ошибках при разработке и отладке, а затем удаляете эти метки после запуска кода.

Если ваш код отображает информацию об ошибке для конечного пользователя, если произошла непредвиденная ошибка, укажите значение изErl в этой информации может сделать поиск и решение проблемы НАМНОГО проще, чем если бы значениеErl не сообщается.

Похожие вопросы