Правильная обработка ошибок в VBA (Excel)
Я работаю с VBA уже довольно давно, но я все еще не уверен в обработке ошибок.
хорошая статья одна из CPearson.com
однако мне все еще интересно, был ли способ, которым я раньше делал ошибки, / совершенно неправильным: блок 1
On Error Goto ErrCatcher
If UBound(.sortedDates) > 0 Then
// Code
Else
ErrCatcher:
// Code
End If
предложение if, потому что если это правда, оно будет выполнено, и если это не удастся, Goto перейдет в Else-part, так как Ubound массива никогда не должно быть нуля или меньше, без ошибки, этот метод работал довольно хорошо до сих пор.
если я правильно понял, это должно быть так: Блок 2
On Error Goto ErrCatcher
If Ubound(.sortedDates) > 0 Then
// Code
End If
Goto hereX
ErrCatcher:
//Code
Resume / Resume Next / Resume hereX
hereX:
или даже такой: Блок 3
On Error Goto ErrCatcher
If Ubound(.sortedDates) > 0 Then
// Code
End If
ErrCatcher:
If Err.Number <> 0 then
//Code
End If
наиболее распространенным способом, который я вижу, является то, что ошибка "Catcher" находится в конце суб, и суб фактически заканчивается до "выхода суб", но, однако, это не немного запутанно, если суб довольно большой, если вы прыгаете вице наоборот, чтобы прочитать код?
блок 4
источник следующего кода: CPearson.com
On Error Goto ErrHandler:
N = 1 / 0 ' cause an error
'
' more code
'
Exit Sub
ErrHandler:
' error handling code'
Resume Next
End Sub
Должно ли это быть как в блоке 3 ?
Спасибо, что прочитали мой вопрос Приветствия skofgar
5 ответов
Я определенно не буду использовать Block1. Кажется неправильным, что блок ошибок в операторе IF не связан с ошибками.
блоки 2,3 & 4, я думаю, вариации на тему. Я предпочитаю использовать блоки 3 & 4 над 2 только из-за нелюбви к оператору GOTO; я обычно использую метод Block4. Это один из примеров кода, который я использую, чтобы проверить, добавлена ли библиотека Microsoft ActiveX Data Objects 2.8 и если не добавить или использовать более раннюю версию, если 2.8 не доступный.
Option Explicit
Public booRefAdded As Boolean 'one time check for references
Public Sub Add_References()
Dim lngDLLmsadoFIND As Long
If Not booRefAdded Then
lngDLLmsadoFIND = 28 ' load msado28.tlb, if cannot find step down versions until found
On Error GoTo RefErr:
'Add Microsoft ActiveX Data Objects 2.8
Application.VBE.ActiveVBProject.references.AddFromFile _
Environ("CommonProgramFiles") + "\System\ado\msado" & lngDLLmsadoFIND & ".tlb"
On Error GoTo 0
Exit Sub
RefErr:
Select Case Err.Number
Case 0
'no error
Case 1004
'Enable Trust Centre Settings
MsgBox ("Certain VBA References are not available, to allow access follow these steps" & Chr(10) & _
"Goto Excel Options/Trust Centre/Trust Centre Security/Macro Settings" & Chr(10) & _
"1. Tick - 'Disable all macros with notification'" & Chr(10) & _
"2. Tick - 'Trust access to the VBA project objects model'")
End
Case 32813
'Err.Number 32813 means reference already added
Case 48
'Reference doesn't exist
If lngDLLmsadoFIND = 0 Then
MsgBox ("Cannot Find Required Reference")
End
Else
For lngDLLmsadoFIND = lngDLLmsadoFIND - 1 To 0 Step -1
Resume
Next lngDLLmsadoFIND
End If
Case Else
MsgBox Err.Number & vbCrLf & Err.Description, vbCritical, "Error!"
End
End Select
On Error GoTo 0
End If
booRefAdded = TRUE
End Sub
у вас есть один действительно замечательный ответ от ray023, но ваш комментарий, что это, вероятно, перебор, подходит. Для "более светлой" версии....
блок 1 это, ИМХО, плохая практика. Как уже указывалось osknows, смешивание обработки ошибок с кодом нормального пути не является хорошим. Во-первых, если a новая ошибка возникает, когда есть условие ошибки в действительности вы будете не получите возможность справиться с этим (если вы звоните из обычной это также имеет обработчик ошибок, где выполнение будет "пузыриться").
Блок 2 похоже на имитацию блока Try / Catch. Это должно быть хорошо, но это не путь VBA. Блок 3 является вариацией на блоке 2.
блок 4 - это голая версия пути VBA. Я бы сильно посоветуйте использовать его или что-то в этом роде, потому что это то, что ожидает любой другой программист VBA, наследующий код. Позвольте представить небольшое расширение, однако:
Private Sub DoSomething()
On Error GoTo ErrHandler
'Dim as required
'functional code that might throw errors
ExitSub:
'any always-execute (cleanup?) code goes here -- analagous to a Finally block.
'don't forget to do this -- you don't want to fall into error handling when there's no error
Exit Sub
ErrHandler:
'can Select Case on Err.Number if there are any you want to handle specially
'display to user
MsgBox "Something's wrong: " & vbCrLf & Err.Description
'or use a central DisplayErr routine, written Public in a Module
DisplayErr Err.Number, Err.Description
Resume ExitSub
Resume
End Sub
обратите внимание, что второй Resume
. Это трюк, которому я научился недавно: это будет никогда выполнить в обычной обработке, так как Resume <label>
оператор отправит выполнение в другое место. Однако это может быть находкой для отладки. При получении уведомления об ошибке выберите отладка (или нажмите Ctl-Break, затем выберите отладка при получении сообщения "выполнение было прервано"). Следующий (выделенный) оператор будет либо MsgBox
или следующий оператор. Используйте "Set Next Statement" (Ctl-F9), чтобы выделить голый Resume
, нажмите клавишу F8. Это покажет вам ровно где произошла ошибка.
Что касается вашего возражения против этого формата "прыжки вокруг", а) это то, что программисты VBA ожидают, как указано ранее, & B) ваши подпрограммы должны быть достаточно коротким, чтобы не далеко прыгать.
две основные цели для обработки ошибок:
- ошибки ловушки вы можете прогнозировать, но не управлять пользователем от выполнения (например, сохранение файла в thumb drive когда thumb диски был удален)
- непредвиденные ошибки, пользователя с формой это сообщает им, в чем проблема есть. Таким образом, они могут передать это сообщение для вас, и вы, возможно, сможете чтобы дать им работу, пока вы работайте над исправлением.
Итак, как бы вы сделали это?
прежде всего, создайте форму ошибки для отображения при возникновении непредвиденной ошибки.
это может выглядеть примерно так (FYI: мой называется frmErrors):
обратите внимание на следующие надписи:
- lblHeadline
- lblSource
- lblProblem
- lblResponse
кроме того, стандартная команда кнопки:
- игнорировать
- повтор
- отмена
нет ничего впечатляющего в коде для этой формы:
Option Explicit
Private Sub cmdCancel_Click()
Me.Tag = CMD_CANCEL
Me.Hide
End Sub
Private Sub cmdIgnore_Click()
Me.Tag = CMD_IGNORE
Me.Hide
End Sub
Private Sub cmdRetry_Click()
Me.Tag = CMD_RETRY
Me.Hide
End Sub
Private Sub UserForm_Initialize()
Me.lblErrorTitle.Caption = "Custom Error Title Caption String"
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
'Prevent user from closing with the Close box in the title bar.
If CloseMode <> 1 Then
cmdCancel_Click
End If
End Sub
в основном, вы хотите знать, какую кнопку пользователь нажал, когда форма закрывается.
затем создайте модуль обработчика ошибок, который будет использоваться во всем приложении VBA:
'****************************************************************
' MODULE: ErrorHandler
'
' PURPOSE: A VBA Error Handling routine to handle
' any unexpected errors
'
' Date: Name: Description:
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'03/22/2010 Ray Initial Creation
'****************************************************************
Option Explicit
Global Const CMD_RETRY = 0
Global Const CMD_IGNORE = 1
Global Const CMD_CANCEL = 2
Global Const CMD_CONTINUE = 3
Type ErrorType
iErrNum As Long
sHeadline As String
sProblemMsg As String
sResponseMsg As String
sErrorSource As String
sErrorDescription As String
iBtnCap(3) As Integer
iBitmap As Integer
End Type
Global gEStruc As ErrorType
Sub EmptyErrStruc_S(utEStruc As ErrorType)
Dim i As Integer
utEStruc.iErrNum = 0
utEStruc.sHeadline = ""
utEStruc.sProblemMsg = ""
utEStruc.sResponseMsg = ""
utEStruc.sErrorSource = ""
For i = 0 To 2
utEStruc.iBtnCap(i) = -1
Next
utEStruc.iBitmap = 1
End Sub
Function FillErrorStruct_F(EStruc As ErrorType) As Boolean
'Must save error text before starting new error handler
'in case we need it later
EStruc.sProblemMsg = Error(EStruc.iErrNum)
On Error GoTo vbDefaultFill
EStruc.sHeadline = "Error " & Format$(EStruc.iErrNum)
EStruc.sProblemMsg = EStruc.sErrorDescription
EStruc.sErrorSource = EStruc.sErrorSource
EStruc.sResponseMsg = "Contact the Company and tell them you received Error # " & Str$(EStruc.iErrNum) & ". You should write down the program function you were using, the record you were working with, and what you were doing."
Select Case EStruc.iErrNum
'Case Error number here
'not sure what numeric errors user will ecounter, but can be implemented here
'e.g.
'EStruc.sHeadline = "Error 3265"
'EStruc.sResponseMsg = "Contact tech support. Tell them what you were doing in the program."
Case Else
EStruc.sHeadline = "Error " & Format$(EStruc.iErrNum) & ": " & EStruc.sErrorDescription
EStruc.sProblemMsg = EStruc.sErrorDescription
End Select
GoTo FillStrucEnd
vbDefaultFill:
'Error Not on file
EStruc.sHeadline = "Error " & Format$(EStruc.iErrNum) & ": Contact Tech Support"
EStruc.sResponseMsg = "Contact the Company and tell them you received Error # " & Str$(EStruc.iErrNum)
FillStrucEnd:
Exit Function
End Function
Function iErrorHandler_F(utEStruc As ErrorType) As Integer
Static sCaption(3) As String
Dim i As Integer
Dim iMCursor As Integer
Beep
'Setup static array
If Len(sCaption(0)) < 1 Then
sCaption(CMD_IGNORE) = "&Ignore"
sCaption(CMD_RETRY) = "&Retry"
sCaption(CMD_CANCEL) = "&Cancel"
sCaption(CMD_CONTINUE) = "Continue"
End If
Load frmErrors
'Did caller pass error info? If not fill struc with the needed info
If Len(utEStruc.sHeadline) < 1 Then
i = FillErrorStruct_F(utEStruc)
End If
frmErrors!lblHeadline.Caption = utEStruc.sHeadline
frmErrors!lblProblem.Caption = utEStruc.sProblemMsg
frmErrors!lblSource.Caption = utEStruc.sErrorSource
frmErrors!lblResponse.Caption = utEStruc.sResponseMsg
frmErrors.Show
iErrorHandler_F = frmErrors.Tag ' Save user response
Unload frmErrors ' Unload and release form
EmptyErrStruc_S utEStruc ' Release memory
End Function
у вас могут быть ошибки, которые будут настроены только для вашего приложения. Это, как правило, будет краткий список ошибок специально только для вашего приложения. Если у вас еще нет модуля констант, создайте модуль, который будет содержать перечисление пользовательских ошибок. (Примечание: Office ' 97 не поддерживает перечисления.). Перечисление должно выглядеть примерно так:
Public Enum CustomErrorName
MaskedFilterNotSupported
InvalidMonthNumber
End Enum
создайте модуль, который выдаст ваши пользовательские ошибки.
'********************************************************************************************************************************
' MODULE: CustomErrorList
'
' PURPOSE: For trapping custom errors applicable to this application
'
'INSTRUCTIONS: To use this module to create your own custom error:
' 1. Add the Name of the Error to the CustomErrorName Enum
' 2. Add a Case Statement to the raiseCustomError Sub
' 3. Call the raiseCustomError Sub in the routine you may see the custom error
' 4. Make sure the routine you call the raiseCustomError has error handling in it
'
'
' Date: Name: Description:
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'03/26/2010 Ray Initial Creation
'********************************************************************************************************************************
Option Explicit
Const MICROSOFT_OFFSET = 512 'Microsoft reserves error values between vbObjectError and vbObjectError + 512
'************************************************************************************************
' FUNCTION: raiseCustomError
'
' PURPOSE: Raises a custom error based on the information passed
'
'PARAMETERS: customError - An integer of type CustomErrorName Enum that defines the custom error
' errorSource - The place the error came from
'
' Returns: The ASCII vaule that should be used for the Keypress
'
' Date: Name: Description:
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'03/26/2010 Ray Initial Creation
'************************************************************************************************
Public Sub raiseCustomError(customError As Integer, Optional errorSource As String = "")
Dim errorLong As Long
Dim errorDescription As String
errorLong = vbObjectError + MICROSOFT_OFFSET + customError
Select Case customError
Case CustomErrorName.MaskedFilterNotSupported
errorDescription = "The mask filter passed is not supported"
Case CustomErrorName.InvalidMonthNumber
errorDescription = "Invalid Month Number Passed"
Case Else
errorDescription = "The custom error raised is unknown."
End Select
Err.Raise errorLong, errorSource, errorDescription
End Sub
теперь вы хорошо оснащены, чтобы ловить ошибки в вашей программе. Вы sub (или функция), должны выглядеть примерно так:
Public Sub MySub(monthNumber as Integer)
On Error GoTo eh
Dim sheetWorkSheet As Worksheet
'Run Some code here
'************************************************
'* OPTIONAL BLOCK 1: Look for a specific error
'************************************************
'Temporarily Turn off Error Handling so that you can check for specific error
On Error Resume Next
'Do some code where you might expect an error. Example below:
Const ERR_SHEET_NOT_FOUND = 9 'This error number is actually subscript out of range, but for this example means the worksheet was not found
Set sheetWorkSheet = Sheets("January")
'Now see if the expected error exists
If Err.Number = ERR_SHEET_NOT_FOUND Then
MsgBox "Hey! The January worksheet is missing. You need to recreate it."
Exit Sub
ElseIf Err.Number <> 0 Then
'Uh oh...there was an error we did not expect so just run basic error handling
GoTo eh
End If
'Finished with predictable errors, turn basic error handling back on:
On Error GoTo eh
'**********************************************************************************
'* End of OPTIONAL BLOCK 1
'**********************************************************************************
'**********************************************************************************
'* OPTIONAL BLOCK 2: Raise (a.k.a. "Throw") a Custom Error if applicable
'**********************************************************************************
If not (monthNumber >=1 and monthnumber <=12) then
raiseCustomError CustomErrorName.InvalidMonthNumber, "My Sub"
end if
'**********************************************************************************
'* End of OPTIONAL BLOCK 2
'**********************************************************************************
'Rest of code in your sub
goto sub_exit
eh:
gEStruc.iErrNum = Err.Number
gEStruc.sErrorDescription = Err.Description
gEStruc.sErrorSource = Err.Source
m_rc = iErrorHandler_F(gEStruc)
If m_rc = CMD_RETRY Then
Resume
End If
sub_exit:
'Any final processing you want to do.
'Be careful with what you put here because if it errors out, the error rolls up. This can be difficult to debug; especially if calling routine has no error handling.
Exit Sub 'I was told a long time ago (10+ years) that exit sub was better than end sub...I can't tell you why, so you may not want to put in this line of code. It's habit I can't break :P
End Sub
копия / вставка кода выше может не работать прямо из ворот, но определенно должна дать вам суть.
кстати, если вам когда-нибудь понадобится, чтобы я сделал логотип вашей компании, посмотрите на меня http://www.MySuperCrappyLogoLabels99.com
Я держу вещи простыми:
На уровне модуля я определяю две переменные и устанавливаю одну в имя самого модуля.
Private Const ThisModuleName As String = "mod_Custom_Functions"
Public sLocalErrorMsg As String
в каждой под / функции модуля я определяю локальную переменную
Dim ThisRoutineName As String
Я установил ThisRoutineName в имя sub или функции
' Housekeeping
On Error Goto ERR_RTN
ThisRoutineName = "CopyWorksheet"
затем я отправляю все ошибки в ERR_RTN: когда они происходят, но сначала я устанавливаю sLocalErrorMsg, чтобы определить, что такое ошибка, и обеспечить некоторую отладку информация.
If Len(Trim(FromWorksheetName)) < 1 Then
sLocalErrorMsg = "Parameter 'FromWorksheetName' Is Missing."
GoTo ERR_RTN
End If
в нижней части каждой под / функции я направляю логический поток следующим образом
'
' The "normal" logic goes here for what the routine does
'
GoTo EXIT_RTN
ERR_RTN:
On Error Resume Next
' Call error handler if we went this far.
ErrorHandler ThisModuleName, ThisRoutineName, sLocalErrorMsg, Err.Description, Err.Number, False
EXIT_RTN:
On Error Resume Next
'
' Some closing logic
'
End If
затем у меня есть отдельный модуль, который я помещаю во все проекты под названием "mod_Error_Handler".
'
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Subroutine Name: ErrorHandler '
' '
' Description: '
' This module will handle the common error alerts. '
' '
' Inputs: '
' ModuleName String 'The name of the module error is in. '
' RoutineName String 'The name of the routine error in in. '
' LocalErrorMsg String 'A local message to assist with troubleshooting.'
' ERRDescription String 'The Windows Error Description. '
' ERRCode Long 'The Windows Error Code. '
' Terminate Boolean 'End program if error encountered? '
' '
' Revision History: '
' Date (YYYYMMDD) Author Change '
' =============== ===================== =============================================== '
' 20140529 XXXXX X. XXXXX Original '
' '
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'
Public Sub ErrorHandler(ModuleName As String, RoutineName As String, LocalErrorMsg As String, ERRDescription As String, ERRCode As Long, Terminate As Boolean)
Dim sBuildErrorMsg As String
' Build Error Message To Display
sBuildErrorMsg = "Error Information:" & vbCrLf & vbCrLf
If Len(Trim(ModuleName)) < 1 Then
ModuleName = "Unknown"
End If
If Len(Trim(RoutineName)) < 1 Then
RoutineName = "Unknown"
End If
sBuildErrorMsg = sBuildErrorMsg & "Module Name: " & ModuleName & vbCrLf & vbCrLf
sBuildErrorMsg = sBuildErrorMsg & "Routine Name: " & RoutineName & vbCrLf & vbCrLf
If Len(Trim(LocalErrorMsg)) > 0 Then
sBuildErrorMsg = sBuildErrorMsg & "Local Error Msg: " & LocalErrorMsg & vbCrLf & vbCrLf
End If
If Len(Trim(ERRDescription)) > 0 Then
sBuildErrorMsg = sBuildErrorMsg & "Program Error Msg: " & ERRDescription & vbCrLf & vbCrLf
If IsNumeric(ERRCode) Then
sBuildErrorMsg = sBuildErrorMsg & "Program Error Code: " & Trim(Str(ERRCode)) & vbCrLf & vbCrLf
End If
End If
MsgBox sBuildErrorMsg, vbOKOnly + vbExclamation, "Error Detected!"
If Terminate Then
End
End If
End Sub
конечный результат-всплывающее сообщение об ошибке, сообщающее мне, в каком модуле, какой soubroutine и какое сообщение об ошибке конкретно было. Кроме того, он также вставит сообщение об ошибке Windows и код.
Блок 2 не работает, потому что он не сбрасывает обработчик ошибок, потенциально вызывая бесконечный цикл. Для правильной работы обработки ошибок в VBA вам нужно Resume
инструкция для очистки обработчика ошибок. The Resume
также активирует предыдущий обработчик ошибок. Блок 2 терпит неудачу, потому что новая ошибка возвращается к предыдущему обработчику ошибок, вызывая бесконечный цикл.
Блок 3 терпит неудачу, потому что нет Resume
оператор, поэтому любая попытка обработки ошибок после этого будет неудача.
каждый обработчик ошибок должен быть завершен выходом из процедуры или Resume
заявление. Маршрутизация обычного выполнения вокруг обработчика ошибок сбивает с толку. Вот почему обработчики ошибок обычно находятся внизу.
но вот еще один способ обработки ошибки в VBA. Он обрабатывает ошибку inline как Try/Catch in VB.net есть несколько подводных камней, но правильно управляемый он работает довольно мило.
Sub InLineErrorHandling()
'code without error handling
BeginTry1:
'activate inline error handler
On Error GoTo ErrHandler1
'code block that may result in an error
Dim a As String: a = "Abc"
Dim c As Integer: c = a 'type mismatch
ErrHandler1:
'handle the error
If Err.Number <> 0 Then
'the error handler has deactivated the previous error handler
MsgBox (Err.Description)
'Resume (or exit procedure) is the only way to get out of an error handling block
'otherwise the following On Error statements will have no effect
'CAUTION: it also reactivates the previous error handler
Resume EndTry1
End If
EndTry1:
'CAUTION: since the Resume statement reactivates the previous error handler
'you must ALWAYS use an On Error GoTo statement here
'because another error here would cause an endless loop
'use On Error GoTo 0 or On Error GoTo <Label>
On Error GoTo 0
'more code with or without error handling
End Sub
источники:
- http://www.cpearson.com/excel/errorhandling.htm
- http://msdn.microsoft.com/en-us/library/bb258159.aspx
ключ к этой работе-использовать Resume
заявление сразу за другим On Error
заявление. The Resume
это в обработчике ошибок и отвлекает код EndTry1
метки. Вы должны немедленно установить другой On Error
оператор, чтобы избежать проблем, как предыдущий обработчик ошибок будет "возобновить". То есть он будет активен и готов к обработке очередной ошибки. Это может привести к повторению ошибки и вводу бесконечного цикла.
чтобы избежать использования предыдущего обработчика ошибок снова, вам нужно установить On Error
для нового обработчика ошибок или просто используйте On Error Goto 0
отменить всю обработку ошибок.