Хорошие шаблоны для обработки ошибок VBA
Какие хорошие шаблоны для обработки ошибок в 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) ...
Я хочу обработать обе ошибки и возобновить выполнение после кода, в котором может произойти ошибка. Кроме того, окончательный код в конце должен ВСЕГДА работать - независимо от того, какие исключения были выбраны ранее. Как я могу достичь этого результата?
Ответы
Ответ 1
Обработка ошибок в VBA
-
On Error Goto
ErrorHandlerLabel -
Resume
(Next
| ErrorHandlerLabel) -
On Error Goto 0
(отключает текущий обработчик ошибок) -
Err
объект
Свойства объекта Err
обычно сбрасываются в ноль или строку нулевой длины в подпрограмме обработки ошибок, но это также может быть сделано явно с Err.Clear
.
Ошибки в процедуре обработки ошибок заканчиваются.
Диапазон 513-65535 доступен для ошибок пользователя. Для пользовательских ошибок класса вы добавляете vbObjectError
к номеру ошибки. См. Документацию MS об Err.Raise
и список номеров ошибок.
Для не реализованных членов интерфейса в производном классе вы должны использовать константу 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
Ответ 2
Я бы добавил:
- Глобальный объект
Err
является самым близким к объекту исключения
- Вы можете эффективно "выбросить исключение" с помощью
Err.Raise
И просто для удовольствия:
-
On Error Resume Next
является воплощением дьявола и его следует избегать, поскольку он молча скрывает ошибки.
Ответ 3
Итак, вы можете сделать что-то вроде этого
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 для изменения потока метода по мере необходимости.
Ответ 4
Вот моя стандартная реализация. Мне нравятся ярлыки как самоописательные.
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
Ответ 5
Professional Excel Development имеет довольно хорошую схему обработки ошибок. Если вы собираетесь проводить время в VBA, это, вероятно, стоит получить книгу. Существует ряд областей, где отсутствует VBA, и в этой книге есть хорошие предложения по управлению этими областями.
PED описывает два метода обработки ошибок. Основная из них - это система, в которой все процедуры точки входа являются подпроцедурами, а все остальные процедуры - это функции, которые возвращают логические значения.
Процедура точки входа используется в операциях Error Error, чтобы фиксировать ошибки в значительной степени, как это было предусмотрено. Процедуры, отличные от точки входа, возвращают True, если ошибок не было и False, если были ошибки. Процедуры, не входящие в точку, также используют On Error.
Оба типа процедур используют центральную процедуру обработки ошибок, чтобы сохранить ошибку в состоянии и зарегистрировать ошибку.
Ответ 6
Я использую кусок кода, который я разработал сам, и он хорош для моих кодов:
В начале функции или sub я определяю:
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
Ответ 7
Здесь довольно приличный шаблон.
Для отладки: при возникновении ошибки нажмите Ctrl-Break (или Ctrl-Pause), перетащите маркер перерыва (или что-то еще, что он вызвал) до строки возобновления, нажмите 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. Вы можете поместить случай выбора в раздел ошибки VBA, если вы хотите ловушку для определенных номеров Err.
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
Ответ 8
В приведенном ниже коде представлен альтернативный вариант, который гарантирует наличие только одной точки выхода для функции sub/.
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
Ответ 9
Также актуальным для обсуждения является относительно неизвестная функция 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
вызывает ошибку, и эта ошибка возникает на 20-й итерации цикла, Erl
возвращает 500
, даже если 600
встречается успешно в предыдущих 19 случаях цикла.
Правильное размещение меток строк в процедуре имеет решающее значение для использования функции Erl
для получения действительно содержательной информации.
В сети есть какое-то количество бесплатных utilies, которые будут автоматически вставлять числовую метку строки в процедуру, поэтому при разработке и отладке вы получите мелкомасштабную информацию об ошибках, а затем удалите эти метки после того, как код начнет жить.
Если ваш код выводит информацию об ошибках конечному пользователю, если возникает непредвиденная ошибка, при условии, что значение от Erl
в этой информации может сделать поиск и исправление проблемы VASTLY проще, чем если не указано значение Erl
.
Ответ 10
Остерегайтесь ловушки слонов:
Я не упоминал об этом в этом обсуждении. [Access 2010]
Как ACCESS/VBA обрабатывает ошибки в объектах CLASS, определяется настраиваемой опцией:
Редактор кода VBA > Инструменты > Параметры > Общие > Ловушка ошибок:
![введите описание изображения здесь]()
Ответ 11
Я считаю, что следующее работает лучше всего, называется центральным подходом к обработке ошибок.
Выгоды
У вас есть 2 режима запуска приложения: отладка и производство. В режиме отладки код остановится при любой непредвиденной ошибке и позволит вам легко выполнить отладку, перейдя к строке, где это произошло, дважды нажав клавишу F8. В производственном режиме пользователю будет отображаться значимое сообщение об ошибке.
Вы можете выдать преднамеренные ошибки, подобные этой, что остановит выполнение кода с сообщением пользователю:
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
колонтитулами, убедившись, что вы указали 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 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 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
Чтобы установить себя в режиме отладки, выполните следующее в окне Immediate:
SetDebugMode True
Ответ 12
Мой личный взгляд на выражение, сделанное ранее в этом потоке:
И просто для удовольствия:
В разделе "Ошибка": "Возобновление ошибок" - это воплощение дьявола, которого следует избегать, поскольку оно скрывает ошибки.
Я использую 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)