Правильная обработка ошибок в VBA (Excel) - PullRequest
54 голосов
/ 17 мая 2011

Я уже давно работаю с VBA, но я все еще не уверен в обработке ошибок.

Хорошая статья - одна из CPearson.com

Однако мне все еще интересно, был ли способ, которым я раньше занимался ErrorHandling, был / является совершенно неправильным: Блок 1

On Error Goto ErrCatcher
   If UBound(.sortedDates) > 0 Then

       // Code

   Else
ErrCatcher:
       // Code

   End If

Предложение if, потому что, если оно истинно, оно будет выполнено, и в случае неудачи Goto перейдет в Else-часть, поскольку 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» находится в конце сабвуфера, а саб фактически заканчивается раньше «Exit Sub», но, тем не менее, не немного ли сбивает с толку Sub довольно большой, если вы прыгаете наоборот, чтобы прочитать код?

Блок 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 ]

50 голосов
/ 19 мая 2011

У вас есть один по-настоящему изумительный ответ от ray023, но ваш комментарий о том, что он, вероятно, излишний, уместен.Для "более легкой" версии ....

Блок 1 , ИМХО, плохая практика.Как уже указывали osknows, смешивать обработку ошибок с кодом с нормальным путем - не хорошо.С одной стороны, если выдается ошибка new , в то время как действует условие ошибки, вы не получаете возможность ее обработать (если вы не вызываете из подпрограммы, которая также имеетобработчик ошибок, где выполнение будет «всплывать»).

Блок 2 выглядит как имитация блока Try / Catch.Это должно быть хорошо, но это не Путь VBA. Блок 3 - это вариант блока 2.

Блок 4 - это базовая версия VBA Way.Я бы настоятельно посоветовал использовать его или что-то в этом роде, потому что этого ожидает любой другой программист 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> отправит выполнение в другое место.Это может быть находкой для отладки.Когда вы получаете уведомление об ошибке, выберите «Отладка» (или нажмите «Ctrl-Break», затем выберите «Отладка» при появлении сообщения «Выполнение было прервано»).Следующим (выделенным) оператором будет либо MsgBox, либо следующий оператор.Используйте «Set Next Statement» (Ctl-F9), чтобы выделить голое Resume, затем нажмите F8.Это покажет вам точно , где была выдана ошибка.

Что касается вашего возражения против этого формата, "прыгающего вокруг", A) это то, чего ожидают программисты VBA, как указано ранее, & B)Ваши подпрограммы должны быть достаточно короткими, чтобы прыгать не далеко.

24 голосов
/ 18 мая 2011

Две основные цели для обработки ошибок:

  1. Перехват ошибок, которые вы можете предсказать, но не можете контролировать пользователя (например, сохранение файла на флэш-накопителе после извлечения флэш-накопителей)
  2. При непредвиденных ошибках предоставьте пользователю форму, сообщающую ему, в чем проблема.Таким образом, они могут передать вам это сообщение, и вы сможете обойти их, пока вы работаете над исправлением.

Итак, как бы вы это сделали?

Прежде всего, создайте форму ошибки, которая будет отображаться при возникновении непредвиденной ошибки.

Это может выглядеть примерно так (FYI: Mine называется frmErrors): Company Error Form

Обратите внимание наследующие метки:

  • 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

Youмогут быть ошибки, которые будут настраиваться только для вашего приложения.Обычно это короткий список ошибок, относящихся только к вашему приложению.Если у вас еще нет модуля констант, создайте его, который будет содержать ENUM ваших пользовательских ошибок.(ПРИМЕЧАНИЕ. Office '97 НЕ поддерживает ENUMS.).ENUM должен выглядеть примерно так:

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

Теперь вы хорошо подготовлены для перехвата ошибок в вашей программе.Вы, субподрядчик (или функция), должны выглядеть примерно так:

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

20 голосов
/ 17 мая 2011

Я бы точно не использовал Block1.Кажется неправильным иметь блок Error в операторе IF, не связанном с ошибками.

Блоки 2,3 и 4 Я думаю, это вариации темы.Я предпочитаю использовать блоки 3 и 4 вместо 2 только из-за неприязни к выражению GOTO;Я обычно использую метод Block4.Это один из примеров кода, который я использую, чтобы проверить, добавлена ​​ли библиотека объектов данных Microsoft ActiveX 2.8 и, если нет, добавить или использовать более раннюю версию, если 2.8 недоступен.

6 голосов
/ 15 октября 2014

У меня все просто:
На уровне модуля я определяю две переменные и устанавливаю одну для имени самого модуля.

    Private Const ThisModuleName            As String = "mod_Custom_Functions"
    Public sLocalErrorMsg                   As String

Внутри каждого Sub / Function модуля я определяю локальную переменную

    Dim ThisRoutineName                     As String

Я установил ThisRoutineName на имя подпрограммы или функции

' 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

Конечным результатом является всплывающее сообщение об ошибке, сообщающее мне, в каком модуле, что такое subroutine и что конкретно было сообщение об ошибке. Кроме того, он также вставит сообщение об ошибке Windows и код.

2 голосов
/ 07 января 2015

Блок 2 не работает, потому что он не сбрасывает обработчик ошибок, потенциально вызывая бесконечный цикл. Для правильной работы с ошибками в VBA вам необходим оператор Resume, чтобы очистить обработчик ошибок. Resume также повторно активирует предыдущий обработчик ошибок. Блок 2 завершается сбоем, поскольку новая ошибка возвращается к предыдущему обработчику ошибок, вызывая бесконечный цикл.

Блок 3 завершается неудачно, потому что нет оператора Resume, поэтому любая попытка обработки ошибок после этого завершится неудачей.

Каждый обработчик ошибок должен завершаться выходом из процедуры или оператора Resume. Маршрутизация нормального выполнения вокруг обработчика ошибок сбивает с толку. Вот почему обработчики ошибок обычно находятся внизу.

Но вот еще один способ обработки ошибки в VBA. Он обрабатывает встроенную ошибку, как Try / Catch в 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

Источники:

Ключом к выполнению этой работы является использование оператора Resume, за которым сразу следует другой оператор On Error. Resume находится в обработчике ошибок и перенаправляет код на метку EndTry1. Вы должны немедленно установить другой оператор On Error, чтобы избежать проблем, так как предыдущий обработчик ошибок "возобновит". То есть он будет активен и готов обработать еще одну ошибку. Это может привести к повторению ошибки и появлению бесконечного цикла.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...