Bons padrões para tratamento de erros VBA

Quais são alguns bons padrões para o tratamento de erros no VBA?

Em particular, o que devo fazer nesta situação:

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

Eu quero lidar com os dois erros e retomar a execução após o código onde o erro pode ocorrer. Além disso, o código final no final deve SEMPRE ser executado – independentemente das exceções lançadas anteriormente. Como posso alcançar este resultado?

Tratamento de erros no VBA

  • On Error Goto ErrorHandlerLabel
  • Resume ( Next | ErrorHandlerLabel )
  • On Error Goto 0 (desativa o manipulador de erros atual)
  • Objeto Err

As propriedades do object Err são normalmente redefinidas para zero ou uma cadeia de comprimento zero na rotina de tratamento de erros, mas também pode ser feita explicitamente com Err.Clear .

Erros na rotina de tratamento de erros estão sendo encerrados.

O intervalo 513-65535 está disponível para erros do usuário. Para erros de class personalizados, você adiciona vbObjectError ao número do erro. Veja a documentação da MS sobre Err.Raise e a lista de números de erros .

Para membros da interface não implementados em uma class derivada , você deve usar a constante 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 

Eu também adicionaria:

  • O object global Err é o mais próximo que você tem de um object de exceção
  • Você pode efetivamente “lançar uma exceção” com Err.Raise

E apenas por diversão:

  • On Error Resume Next é o diabo encarnado e deve ser evitado, pois oculta silenciosamente os erros

Então você poderia fazer algo assim

 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 

Se você quiser assar em exceções personalizadas. (por exemplo, aqueles que violam as regras de negócios) use o exemplo acima, mas use o goto para alterar o stream do método conforme necessário.

Aqui está minha implementação padrão. Eu gosto que os labels sejam auto-descritivos.

 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 

Ou, com um bloco Finally :

 Public Sub DoSomething() On Error GoTo Catch ' Try ' normal code here GoTo Finally Catch: 'error code Finally: 'cleanup code End Sub 

O Excel Professional Development possui um bom esquema de tratamento de erros . Se você vai gastar algum tempo em VBA, provavelmente vale a pena receber o livro. Há uma série de áreas onde o VBA está faltando e este livro tem boas sugestões para gerenciar essas áreas.

O PED descreve dois methods de tratamento de erros. O principal deles é um sistema onde todos os procedimentos de ponto de input são subprocedimentos e todos os outros procedimentos são funções que retornam booleanos.

O procedimento de ponto de input usa as instruções On Error para capturar os erros da maneira mais adequada. Os procedimentos de ponto de input não retornam True se não houver erros e False se houver erros. Procedimentos de ponto de não input também usam On Error.

Ambos os tipos de procedimentos usam um procedimento central de tratamento de erros para manter o erro no estado e registrar o erro.

Aqui está um padrão bastante decente.

Para debugging: Quando um erro é levantado, pressione Ctrl-Break (ou Ctrl-Pause), arraste o marcador de pausa (ou seja qual for o nome) para a linha Resume, pressione F8 e você irá para a linha que “lançou” o erro.

O ExitHandler é o seu “Finalmente”.

Ampulheta será morta todas as vezes. O texto da barra de status será apagado todas as vezes.

 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 

Ele também intercepta os erros do DAO e do VBA. Você pode colocar um Select Case na seção de erros do VBA se quiser capturar números de Err específicos.

 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 

Eu uso um pedaço de código que eu mesmo desenvolvi e é muito bom para os meus códigos:

No começo da function ou sub, eu defino:

 On error Goto ErrorCatcher: 

e então, eu lido com os possíveis erros

 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 

Minha opinião pessoal sobre uma declaração feita anteriormente neste tópico:

E apenas por diversão:

On Error Resume Next é o diabo encarnado e deve ser evitado, pois oculta silenciosamente os erros.

Estou usando o On Error Resume Next em procedimentos nos quais não quero que um erro pare meu trabalho e onde qualquer instrução não dependa do resultado das instruções anteriores.

Quando estou fazendo isso, adiciono uma variável global debugModeOn e a defino como True . Então eu uso assim:

 If not debugModeOn Then On Error Resume Next 

Quando eu entrego meu trabalho, defino a variável como false, ocultando os erros apenas para o usuário e exibindo-os durante o teste.

Também usá-lo ao fazer algo que pode falhar como chamar o DataBodyRange de um ListObject que pode estar vazio:

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

Ao invés de:

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

Ou verificar a existência de um item em uma coleção:

 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) 

Cuidado com a armadilha do elefante:

Eu não vi nenhuma menção a isso nesta discussão. [Access 2010]

Como o ACCESS / VBA manipula erros em objects CLASS é determinado por uma opção configurável:

Editor de Códigos do VBA> Ferramentas> Opções> Geral> Interceptação de Erros:

insira a descrição da imagem aqui

O código abaixo mostra uma alternativa que garante que há apenas um ponto de saída para o sub / function.

 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 

Também relevante para a discussão é a function relativamente desconhecida de Erl . Se você tiver labels numéricos em seu procedimento de código, por exemplo,

 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 

A function Erl retorna o label da linha numérica encontrado mais recentemente. No exemplo acima, se ocorrer um erro em tempo de execução após o label 1200: mas antes de 1300: Erl function Erl retornará 1200 , já que esse é o label de linha mais recente encontrado com êxito. Acho que é uma boa prática colocar um label de linha imediatamente acima do bloco de tratamento de erros. Eu normalmente uso 9999 para indicar que a parte principal do procedimento correu para a conculação esperada.

NOTAS:

  • Os labels de linha DEVEM ser inteiros positivos – um label como MadeItHere: não é reconhecido por Erl .

  • Os labels de linha são completamente não relacionados aos números de linha reais de um VBIDE CodeModule de VBIDE CodeModule . Você pode usar qualquer número positivo desejado, na ordem que desejar. No exemplo acima, existem apenas 25 linhas de código, mas os números dos labels de linha começam em 1000 . Não há relação entre números de linha de editor e números de etiqueta de linha usados ​​com Erl .

  • Números de labels de linha não precisam estar em nenhuma ordem específica, embora, se não estiverem em ordem ascendente de cima para baixo, a eficácia e o benefício de Erl diminuem bastante, mas Erl ainda informará o número correto.

  • Os labels de linha são específicos para o procedimento em que aparecem. Se o procedimento ProcA chamar o procedimento ProcB e ocorrer um erro no ProcB que passa o controle de volta para ProcA , Erl (no ProcA ) retornará o número de etiqueta de linha encontrado mais recentemente no ProcA antes de chamar ProcB . De dentro da ProcA , você não pode obter os números de label de linha que podem aparecer no ProcB .

Tenha cuidado ao colocar etiquetas de números de linha em um loop. Por exemplo,

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

Se o código seguinte ao label de linha 500 mas anterior a 600 causar um erro e esse erro surgir na 20a iteração do loop, Erl retornará 500 , mesmo que 600 tenha sido encontrado com êxito nas 19 interações anteriores do loop.

A colocação adequada dos labels de linha dentro do procedimento é fundamental para usar a function Erl para obter informações verdadeiramente significativas.

Há vários utilitários gratuitos na rede que inserem automaticamente o label de linha numérica em um procedimento, para que você tenha informações de erros refinadas durante o desenvolvimento e a debugging e, em seguida, remova esses labels quando o código for ativado.

Se o seu código exibir informações de erro para o usuário final se ocorrer um erro inesperado, fornecer o valor de Erl nessa informação pode tornar a localização e correção do problema VASTLY mais simples do que se o valor de Erl não for relatado.

Acho que o seguinte funciona melhor, chamado de abordagem central de tratamento de erros.

Benefícios

Você tem dois modos de executar seu aplicativo: Depurar e Produção . No modo de debugging , o código irá parar em qualquer erro inesperado e permitir que você depure facilmente saltando para a linha onde ocorreu, pressionando F8 duas vezes. No modo de produção , uma mensagem de erro significativa será exibida ao usuário.

Você pode jogar erros intencionais como este, o que interromperá a execução do código com uma mensagem para o usuário:

 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 

Implementação

Você precisa “agrupar” todas as sub-rotinas e funções com qualquer quantidade significativa de código com os seguintes headers e rodapés, certificando-se de especificar ehCallTypeEntryPoint em todos os seus pontos de input. Observe também a constante msModule , que precisa ser colocada em todos os módulos.

 Option Explicit Const msModule As String = "" ' 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 

O conteúdo do módulo central do manipulador de erros é o seguinte:

 ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' ' 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 = "" 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 = "" Private Const msDEBUG_MODE_SECTION = "" 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 

Para definir você mesmo no modo de debugging , execute o seguinte na janela Immediate:

 SetDebugMode True