El código a continuación se explica por sí mismo: simplemente cópielo y péguelo todo en un módulo y ejecútelo , proporciona algunos casos de uso y muchos comentarios explicativos en el texto. (Funciona, pero me interesa saber qué opinan otras personas y cualquier sugerencia que le gustaría hacer.)
Los hechos más importantes a tener en cuenta son:
-
Cuando usa un error goto Label1, el procedimiento entra en un estado de «Estoy» manejando un error «ya que se ha generado una excepción. Cuando está en este estado, si se ejecuta otra instrucción label2 «On Error Goto», NO irá a label2, pero se genera un error que se pasa al código que llamó al procedimiento.
-
Puede evitar que un procedimiento esté en el estado «Estoy manejando un error» borrando la excepción (estableciendo err en nada para que la propiedad err.number se convierta en 0) usando
Err.clear or On Error Goto -1 " Which I think is less clear!
(NOTA que On Error Goto 0
es diferente de lo anterior)
También es importante tener en cuenta que Err.Clear lo restablece a cero, pero en realidad es equivalente a:
On Error Goto -1 On Error Goto 0
es decir, Err.Clear elimina un «On Error Goto» que está actualmente en su lugar. Por lo tanto, es mejor usar:
On Error Goto -1
como si se usara Err.clear. escribir
Err.Clear On Error Goto MyErrorHandlerLabel
Utilizo las técnicas anteriores con varias etiquetas para simular la funcionalidad a veces útil que brindan los bloques TRY CATCH de Visual basic, que creo que tienen su lugar en la escritura código legible.
Es cierto que esta técnica crea algunas líneas más de código que una buena declaración VB try catch, pero no es demasiado complicado y bastante fácil de conseguir r dirígete.
PD. También puede ser de interés el procedimiento ManageErrSource que hace que la propiedad Err.Source almacene el procedimiento donde ocurrió el error.
Option Compare Database Option Explicit Dim RememberErrNumber As Long Dim RememberErrDescription As String Dim RememberErrSource As String Dim RememberErrLine As Integer Private Sub RememberThenClearTheErrorObject() On Error Resume Next " For demo purposes Debug.Print "ERROR RAISED" Debug.Print Err.Number Debug.Print Err.Description Debug.Print Err.Source Debug.Print " " " This function has to be declared in the same scope as the variables it refers to RememberErrNumber = Err.Number RememberErrDescription = Err.Description RememberErrSource = Err.Source RememberErrLine = Erl() " Note that the next line will reset the error object to 0, the variables above are used to remember the values " so that the same error can be re-raised Err.Clear " Err.Clear is used to clear the raised exception and set the err object to nothing (ie err.number to 0) " If Err.Clear has not be used, then the next "On Error GoTo ALabel" that is used in this or the procedure that called it " will actually NOT pass execution to the ALabel: label BUT the error is paseed to the procedure that called this procedure. " Using Err.Clear (or "On Error GoTo -1 ") gets around this and facilitates the whole TRY CATCH block scenario I am using there. " For demo purposes Debug.Print "ERROR RAISED is now 0 " Debug.Print Err.Number Debug.Print Err.Description Debug.Print Err.Source Debug.Print " " " For demo purposes Debug.Print "REMEMBERED AS" Debug.Print RememberErrNumber Debug.Print RememberErrDescription Debug.Print RememberErrSource Debug.Print " " End Sub Private Sub ClearRememberedErrorObjectValues() " This function has to be declared in the same scope as the variables it refers to RememberErrNumber = 0 RememberErrDescription = "" RememberErrSource = "" RememberErrLine = 0 End Sub Sub ExampleOfTryCatchBlockInVBA() On Error GoTo HandleError " ----------------------------------------------------- " SubProcedure1 has the example of a multiple line TRY block with a block of code executed in the event of an error SubProcedure1 Exit Sub HandleError: Select Case Err.Number Case 0 " This shold never happen as this code is an error handler! " However if it does still allow the Err.raise to execute below. (In this case Err.raise will fail " and itself will raise an error "Invalid procedure call or argument" indicating that 0 cannot be used to raise and error! Case 111111 " You might want to do special error handling for some predicted error numbers " perhaps resulting in a exit sub with no error or " perhaps using the Err.raise below Case Else " Just the Err.raise below is used for all other errors End Select " " I include the procedure ManageErrSource as an exmple of how Err.Source can be used to maintain a call stack of procedure names " and store the name of the procedure that FIRST raised the error. " Err.Raise Err.Number _ , ManageErrSource("MyModuleName", Err.Source, Erl(), "tsub1_RaisesProcedureNotFoundError") _ , Err.Number & "-" & Err.Description " Note the next line never gets excuted, but I like to have resume in the code for when I am debugging. " (When a break is active, by moving the next executable line onto it, and using step over, it moves the exection point to the line that actually raised the error) Resume End Sub Sub SubProcedure1() " ----------------------------------------------------- " Example of a multiple line TRY block with a Case statement used to CATCH the error " " It is sometimes better to NOT use this technique but to put the code in it"s own procedure " (ie I refer to the code below that is surrounded by the tag #OWNSUB) . " However,sometimes using this technique makes code more readable or simpler! " Dim i As Integer " This line puts in place the defualt error handler found at the very foot of the procedure On Error GoTo HandleError " " Perhaps lots of statements and code here " " First an example with comments " ----------------------------------------------------- " TRY BLOCK START " This next line causes execution to "jump" to the "catch" block in the event an error is detected. On Error GoTo CatchBlock1_Start " #OWNSUB tsub_WillNotRaiseError_JustPrintsOk If vbYes = MsgBox("1. Do you want to raise an error in the try block? - (PRESS CTRL+BREAK now then choose YES, try no later.)", vbYesNo) Then i = 100 / 0 End If " " Perhaps lots of statements and code here " " #OWNSUB " TRY BLOCK END " ----------------------------------------------------- " ----------------------------------------------------- " CATCH BLOCK START CatchBlock1_Start: If Err.Number = 0 Then On Error GoTo HandleError " Re-instates the procedure"s generic error handler " This is also done later, but I think putting it here reduces the likelyhood of a coder accidentally removing it. Else " WARNING: BE VERY CAREFUL with any code that is written here as " the "On Error GoTo CatchBlock1_Start" is still in effect and therefore any errors that get raised could goto this label " and cause and infinite loop. " NOTE that a replacement "On Error Goto" cannot be executed until Err.clear is used, otherwise the "On Error Goto" " will itself raise and error. " THEREFORE KEEP THE CODE HERE VERY SIMPLE! " RememberThenClearTheErrorObject should be the only code executed and this called procedure must be tight! " This saves the details of the error in variables so that the "On Error GoTo HandleError" can be used " to determine how the next Err.Raise used below is handled (and also how any unexpected implicitly raised errors are handled) RememberThenClearTheErrorObject On Error GoTo HandleError "#THISLINE# If vbYes = MsgBox("2. Do you want to raise an error in the erro handler? - (PRESS CTRL+BREAK now then try both YES and NO )", vbYesNo) Then i = 100 / 0 End If Select Case RememberErrNumber Case 0: " No Error, do Nothing Case 2517 Debug.Print "The coder has decided to just give a Warning: Procedure not found " & Err.Number & " - " & Err.Description ClearRememberedErrorObjectValues " Not essential, but might save confusion if coding errors are made Case Else " An unexepected error or perhaps an (user) error that needs re-raising occurred and should to be re-raised " NOTE this is giving an example of what woudl happen if the CatchBlock1_ErrorElse is not used below If vbYes = MsgBox("3. Do you want to raise an error in the ELSE error handler? CatchBlock1_ErrorElse *HAS NOT* been used? - (PRESS CTRL+BREAK now then try both YES and NO )", vbYesNo) Then i = 100 / 0 End If On Error GoTo CatchBlock1_ErrorElse " SOME COMPLEX ERROR HANDLING CODE - typically error logging, email, text file, messages etc.. " Because the error objects values have been stored in variables, you can use " code here that might itself raise an error and CHANGE the values of the error object. " You might want to surround the code with the commented out CatchBlock1_ErrorElse lines " to ignore these errors and raise the remembered error. (or if calling a error handling module " just use on error resume next). " Without the CatchBlock1_ErrorElse lines any error raised in this "complex code" will be handled by the " active error handler which was set by the "On Error GoTo HandleError" tagged as "#THISLINE#" above. If vbYes = MsgBox("4. Do you want to raise an error in the ELSE error handler when CatchBlock1_ErrorElse HAS been used? - (PRESS CTRL+BREAK now then try both YES and NO )", vbYesNo) Then i = 100 / 0 End If CatchBlock1_ErrorElse: On Error GoTo HandleError " This line must be preceeded by an new "On error goto" for obvious reasons Err.Raise RememberErrNumber, RememberErrSource, RememberErrDescription End Select On Error GoTo HandleError End If " CATCH BLOCK END " ----------------------------------------------------- On Error GoTo HandleError " Unnecessary but used to delimt the catch block " " lots of code here perhaps " " ----------------------------------------------------- " Example 2 " " In this example goto statements are used instead of the IF statement used in example 1 " and no explanitory comments are given (so you can see how simple it can look) " " ----------------------------------------------------- " TRY BLOCK START On Error GoTo CatchBlock2_Start tsub_WillNotRaiseError_JustPrintsOk If vbYes = MsgBox("Do you want to raise an error? - (PRESS CTRL+BREAK now then choose YES)", vbYesNo) Then i = 100 / 0 End If " " Perhaps lots of statements and code here " " TRY BLOCK END " ----------------------------------------------------- GoTo CatchBlock2_End: CatchBlock2_Start: RememberThenClearTheErrorObject On Error GoTo HandleError Select Case RememberErrNumber Case 0: " No Error, do Nothing Case 2517 Debug.Print "The coder has decided to just give a Warning: Procedure not found " & Err.Number & " - " & Err.Description ClearRememberedErrorObjectValues " Not essential, but might save confusion if coding errors are made Case Else " An unexepected error or perhaps an (user) error that needs re-raising occurred and should to be re-raised " In this case the unexpecetd erro will be handled by teh code that called this procedure " This line must be preceeded by an new "On error goto" for obvious reasons Err.Raise RememberErrNumber, RememberErrSource, RememberErrDescription End Select On Error GoTo HandleError End If CatchBlock2_End: " CATCH BLOCK END " ----------------------------------------------------- On Error GoTo HandleError " Unnecessary but used to delimt the catch block " " Here you could add lots of lines of vba statements that use the generic error handling that is after the HandleError: label " " " " You could of course, alway add more TRY CATCH blocks like the above " " Exit Sub HandleError: Select Case Err.Number Case 0 " This shold never happen as this code isan error handler! " However if it does still allow the Err.raise to execute below. (In this case Err.raise will fail " and itself will raise an error "Invalid procedure call or argument" indicating that 0 cannot be used to raise and error! Case 111111 " You might watch to do special error handling for some predicted error numbers " perhaps exit sub " Perhaps using the Err.raise below End Select " ie Otherwise " " Note that I use the Err.Source to maintain a call stack of procedure names " Err.Raise Err.Number _ , ManageErrSource("MyModuleName", Err.Source, Erl(), "tsub1_RaisesProcedureNotFoundError") _ , Err.Number & "-" & Err.Description " Note the next line never gets excuted, but I like to have resume in the code for when I am debugging. " (By moving the next executable line onto it, and using step over, it moves the exection point to the line that actually raised the error) Resume End Sub Sub tsub_WillNotRaiseError_JustPrintsOk() Static i As Integer i = i + 1 Debug.Print "OK " & i End Sub Public Function ManageErrSource(MyClassName As String, ErrSource As String, ErrLine As Integer, ProcedureName As String) As String " This function would normally be in a global error handling module " On Error GoTo err_ManageErrSource Const cnstblnRecordCallStack As Boolean = True Select Case ErrSource Case Application.VBE.ActiveVBProject.Name " Err.Source is set to this value when a VB statement raises and error. eg In Access by defualt it is set to "Database" ManageErrSource = Application.VBE.ActiveVBProject.Name & " " & MyClassName & "." & ProcedureName & ":" & ErrLine Case "" " When writing code ouside of the error handling code, the coder can raise an error explicitly, often using a user error number. " ie by using err.raise MyUserErrorNumber, "", "My Error descirption". " The error raised by the coder will be handled by an error handler (typically at the foot of a procedure where it was raised), and " it is this handler that calls the ManageErrSource function changing the Err.Source from "" to a meaningful value. ManageErrSource = Application.VBE.ActiveVBProject.Name & " " & MyClassName & "." & ProcedureName & ":" & ErrLine Case Else " This code is executed when ManageErrSource has already been called. The Err.Source will already have been set to hold the " Details of where the error occurred. " This option can be used to show the call stack, ie the names of the procdures that resulted in the prcedure with the error being called. If cnstblnRecordCallStack Then If InStr(1, ErrSource, ";") = 0 Then ManageErrSource = ErrSource & ":: Called By: " End If ManageErrSource = ErrSource & ";" & ProcedureName & ":" & ErrLine Else ManageErrSource = ErrSource End If End Select Exit Function err_ManageErrSource: Err.Raise Err.Number, "MyModuleName.err_ManageErrSource", Err.Description Resume End Function
Comentarios
- Solo dos comentarios: 1 ¿Por qué demonios usarías esto? 2
On Error Goto -1
- Try / catch también se puede emular envolviendo el código relevante con
On Error Resume Next
yOn Error GoTo 0
y verificandoErr.Number
. Lo anterior es algo difícil de seguir, tiene unaspaghetti
estructura .. - Gracias Rory, yo ‘ lo he cambiado. Lo usaría o la misma razón por la que cualquiera usaría una instrucción TRY CATCH en VB o SQL Server. es decir, le permite estructurar su código de manera diferente. es decir, puede usar el mismo controlador de errores para muchas líneas de código sin tener que poner las líneas en su propio procedimiento.
- Loannis. sí, ‘ he hecho eso en el pasado, para líneas individuales de código que necesitan un controlador de errores. Es decir, una línea de código tiene un controlador de errores. TRY CATCH permite incrustar un bloque de código (con muchas líneas) en un procedimiento con su propio controlador de errores ‘. Yo uso mucho TRY CATCH en SQL Server y como ‘ está disponible en VB, también debe servir para algún propósito generalmente útil. Es cierto que esta versión es un poco desordenada.
- @Loannis ¿Qué sucede si desea omitir varias líneas cuando obtiene un error? Vea mi respuesta para un ejemplo simplificado. Por supuesto, también puede hacer eso con el manejo regular de errores.
Respuesta
El problema es que los errores de tiempo de ejecución en VBA no son excepciones , y el manejo de errores en VBA tiene muy poco en común con el manejo de excepciones.
RememberErrLine = Erl()
El Erl
La función es un miembro oculto del módulo VBA.Information
por una razón: devuelve 0 a menos que el error haya ocurrido en una línea numerada. Y si está usando números de línea en VBA, ha estado viviendo en una cueva durante 25 años y probablemente esté usando declaraciones GoSub
en lugar de escribir procedimientos. Los números de línea son compatibles por razones heredadas / de compatibilidad con versiones anteriores , porque el código escrito en la década de 1980 los requería .
Me gusta cómo lo dijo usted mismo:
" THEREFORE KEEP THE CODE HERE VERY SIMPLE!
… pero ¿por qué no se aplica al resto del código? No te ofendas, pero esto es una lógica espagueti, escrita en procedimientos que violan clara y descaradamente las Principio de responsabilidad única . Ningún código compatible con SRP necesitaría dos de estos bloques » try-catch «.
Esto huele:
Case 0: " No Error, do Nothing
Significa una de dos cosas: o tiene un código de manejo de errores que se ejecuta en contextos sin errores, o tiene un código muerto que debe eliminarse.
Esto huele apesta :
GoTo CatchBlock2_End: CatchBlock2_Start:
En primer lugar, ac olon (:
) que no especifica una etiqueta de línea , es un separador de instrucciones .Resulta que una nueva línea también es un » separador de instrucciones «, por lo que los dos puntos al final de GoTo CatchBlock2_End
es completamente inútil y confuso, especialmente dado el nivel de sangría de la declaración GoTo .
Hablando de GoTo
…
Neil Stephenson cree que es lindo nombrar sus etiquetas» dengo «
No me gusta cómo necesito saltar entre etiquetas para seguir el código. En mi opinión, es desordenado e innecesariamente espaguetizado.
Bien, smartypants . Entonces, ¿cómo se manejan limpiamente los errores en VBA?
1. Escriba código limpio en primer lugar.
Cumpla con las mejores prácticas y escriba procedimientos pequeños que hagan una cosa y que lo hagan bien.
2. Escribe código orientado a objetos.
Abstracción y encapsulación son dos de los 4 pilares de OOP , y son totalmente compatibles con VBA. Polimorfismo también es una opción; solo se descarta la herencia adecuada, pero eso no impide que uno abstraiga conceptos en módulos de clase y creación de instancias de objetos especializados.
El código de procedimiento escrito en módulos estándar (.bas) debe ser pequeños métodos públicos (macro » ganchos «) que crean los objetos necesarios para ejecutar la funcionalidad.
Entonces, ¿cómo se relaciona eso incluso de forma remota con el manejo adecuado de errores?
3. Adopte el manejo idiomático de errores, no lo luche.
Dado que el código se adhiere a los puntos anteriores, no hay razón para no implementar el manejo de errores la forma idiomática de VBA.
Public Sub DoSomething() On Error GoTo CleanFail "method body CleanExit: "cleanup code goes here. runs regardless of error state. Exit Sub CleanFail: "handle runtime error(s) here. "Raise Err.Number ""rethrow" / "bubble up" Resume CleanExit Resume "for debugging - break above and jump to the error-raising statement End Sub
Este patrón es análogo a un » try-catch-finalmente » de la siguiente manera:
- El cuerpo es el » try » parte, que hace lo que dice el nombre del método y nada más
-
CleanFail
es el » captura » parte, que solo se ejecuta si se genera un error -
CleanExit
es la » finalmente » parte, que se ejecuta independientemente de si se generó un error o no … a menos que «vuelva a relanzar . Pero si necesita hacer aparecer un error para que el código de llamada lo maneje, no debería tener mucha limpieza c ode para ejecutar, y debería tener una muy, muy buena razón para hacerlo.
Si su subrutina de manejo de errores puede generar un error, entonces no está cumpliendo con SRP. Por ejemplo, escribir en un archivo de registro es una preocupación en sí misma, que debe resumirse en algún objeto Logger
que viva para lidiar con los problemas de registro y exponga métodos que manejan sus propios errores. . El código de la subrutina de manejo de errores debe ser trivial.
Comentarios
Responder
Escuchar Mat «sMug , pero no cubrió la situación en la que realmente sabe cómo recuperarse de un error. Para ser más completo, me gustaría cubrir eso.
Primero veamos cómo haríamos algo como esto en VB.Net.
Try foo = SomeMethodLikelyToThrowAnException Catch e As SomeException foo = someDefaultValue End Try " some more code
La forma idiomática de hacer esto en VB6 es ResumeNext
. Escribe esto, porque es la solo vez que lo diré directamente a ResumeNext
.
On Error Goto ErrHandler foo = SomeMethodLikelyToRaiseAnError " some more code CleanExit: " clean up resources Exit Sub ErrHandler: If Err.Number = ConstantValueForErrorWeExpected Then foo = someDefaultValue Resume Next End If Resume CleanExit "or re-raise error Exit Sub
La forma alternativa es incorporar esta lógica, que creo que es un poco más limpia y más cercano al Try...Catch
modismo, pero puede volverse feo rápidamente si se abusa de él.
On Error Resume Next foo = SomeMethodLikelyToRaiseAnError If Err.Number = ConstantValueForErrorWeExpected Then foo = someDefaultValue End If On Error Goto 0
O es un forma idiomática de lidiar con los errores esperados, pero hagas lo que hagas. No te molestes con Resume Next
hasta que entiendas completamente lo que hace y cuándo es apropiado . (Es más una advertencia para los futuros lectores que para usted. Parece comprender completamente el manejo de errores en VB6. Quizás demasiado bien para su propio bien).
Comentarios
- Gracias @RubberDuck por sus útiles comentarios. Siendo honesto, me encuentro usando » En caso de error, reanude el siguiente » antes bastantes llamadas de procedimiento después r que normalmente hay un SELECT CASE que responde a cualquier error que se presente. El gran error que me doy cuenta de que estoy cometiendo es que levanto una excepción definida por el usuario en el subprocedimiento para marcar situaciones que surgen (como el usuario que solicita cancelar el procesamiento). Creo que debería usar más funciones. Esto es una indicación de que mi estructura de código general es » no ideal » / poor y creo que y necesito abordar esto. Gracias.
- Usted ‘ ha acertado en un gran punto @HarveyFrench. Las excepciones son para el comportamiento excepcional , no para controlar el flujo. Bienvenido a CR.
- Yo ‘ estaría muy interesado en sus opiniones sobre esta pregunta SO: stackoverflow. com / questions / 31007009 / …
- El código que recibí de fmsinc.com soluciona muchos de los problemas que ‘ que hemos tenido. ‘ Valoro tu opinión. Vea aquí codereview.stackexchange.com/questions/94498/…
Respuesta
Esta respuesta pretende simplificar el patrón Try / Catch para que sea fácilmente comprensible.
Esto no es muy diferente de manejo regular de errores en línea, excepto que puede omitir varias líneas a la vez, manejar un error y luego reanudar la ejecución regular. Este es un patrón estructurado muy limpiamente para manejar un error. El flujo se mueve muy limpiamente de arriba a abajo; aquí no hay código espagueti.
Tradicionalmente, el controlador de errores se coloca en la parte inferior. Pero la construcción Try / Catch es muy elegante. Es una forma muy estructurada de manejar errores y es muy fácil de seguir. Este patrón intenta reproducir eso de una manera muy clara y concisa. El flujo es muy consistente y no salta de un lugar a otro.
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 is now active Debug.Print (Err.Description) End If "disable previous error handler (VERY IMPORTANT) On Error GoTo 0 "exit the error handler Resume EndTry1 EndTry1: "more code with or without error handling End Sub
Fuentes:
- Manejo de errores de Pearson en VBA
- Cómo: Manejar errores en tiempo de ejecución en VBA
- Manejo adecuado Errores en VBA (Excel)
- Mi propio: Cómo hacer un bloque de manejo de errores en línea como Try / Catch
Si se administra correctamente, esto funciona bastante bien. Es un patrón de flujo muy limpio que se puede reproducir en cualquier lugar que se necesite.
Comentarios
- @D_Bester, Gracias por los enlaces y el ejemplo simple. ‘ todavía estoy aprendiendo y sus comentarios me parecieron útiles; sin embargo, deberá agregar un » en caso de error Goto 0 » después del » en el Error goto -1 «. También pensándolo bien, creo que es mejor usar Err.Clear en lugar de » On Error Goto -1 » ya que muestra más claramente lo que está sucediendo. ‘ Estoy encontrando todo este manejo de errores en VBA como un arte negro.
- @D_Bester. Reflexionando, su código está bien si todo lo que desea es darle al usuario un mensaje cuando ocurre un error, pero ¿qué sucede si desea volver a generar el error? Cuál será un escenario muy común. Considerar.Si su código estaba tratando de buscar los detalles de un cliente ‘ y no pudo ‘ obtenerlos por una razón INESPERADA. Debería volver a generar el error y dejar que el código que está usando su código para realizar la búsqueda decida qué hacer.
- @HarveyFrench Si desea volver a generar el error, simplemente use ‘ Err.Raise ‘. No hay problema asumiendo que el código está bien estructurado y el manejo de errores está habilitado en el código de llamada.
- @HarveyFrench
Err.Clear
yOn Error Goto -1
NO son equivalentes. Consulta stackoverflow.com/a/30994055/2559297 - Tienes ‘ tienes razón. no es lo mismo lo siento. Pero creo que el código anterior todavía necesita On Error GoTo -1 reemplazado con Err.Clear; de lo contrario, » ‘ más código sin manejo de errores » saltará a ErrHandler1 si ocurre un error.
Responder
Con respecto a «CleanSalir» y el tema «Finalmente».
La taza de Mat escribió:
CleanSalir es el » finalmente » parte, que se ejecuta independientemente de si se generó un error o no … a menos que «vuelva a generar.
Tal situación podría ocurrir, por ejemplo, en este código de procedimiento:
Enfoque de procedimiento
Public Sub DoSomething() On Error GoTo CleanFail " Open any resource " Use the resource CleanExit: " Close/cleanup the resource Exit Sub CleanFail: Raise Err.Number Resume CleanExit End Sub
Problema aquí : si se produce algún error en el cuerpo de los métodos que deba volver a aparecer en CleanFail, CleanSalir no se ejecutará Todo y, por lo tanto, el recurso no se puede cerrar correctamente.
Claro, también podría cerrar el recurso en el mismo controlador de errores, pero eso podría llevar a tener múltiples fragmentos de código donde el manejo de recursos será / ha
Mi sugerencia es utilizar un objeto personalizado para cada necesidad de vinculación de recursos:
AnyResourceBindingClass
Private Sub Class_Initialize() "Or even use Mats "Create method" approach here instead. "Open/acquire the resource here End Sub Private Sub Class_Terminate() On Error GoTo CleanFail "Close/clean up the resource here properly CleanExit: Exit Sub CleanFail: MsgBox Err.Source & " : " & Err.Number & " : " & Err.Description Resume CleanExit End Sub Public Sub UseResource() "Do something with the resource End Sub
Enfoque orientado a objetos
Public Sub DoSomething() On Error GoTo CleanFail " Use custom object which acquires the resource With New AnyResourceBindingClass .UseResource End With CleanExit: Exit Sub CleanFail: Raise Err.Number Resume CleanExit End Sub
Oportunidad : Debido a que el objeto personalizado estará fuera de alcance después de que se genere el error, su método Terminate se ejecutará automáticamente, lo que hace que el recurso adquirido se cierre / limpie correctamente.
Una necesidad menos para un bloque «finalmente».
Manejo de errores en el método Terminate
En mi opinión, depende del contexto cómo se manejará un error en el método Terminate de la clase personalizada. ¿Quizás debería registrarse en algún lugar adicionalmente o incluso tragarse?
Sin duda, esto es discutible.
Pero es esencial para habilitar un controlador de errores en este método, porque, hasta donde yo sé, cualquier error no controlado en este método hará que VBA interrumpa la ejecución y muestre su cuadro de mensaje de error de tiempo de ejecución estándar.
Respuesta
Para aclarar mi publicación anterior, la siguiente línea del código de HarveyFrench:
RememberErrLine = Erl()
no funcionará a menos que se hayan agregado números de línea a cada línea de código. En lugar de escribir manualmente los números de línea, lo cual es demasiado tedioso, puede usar una herramienta para agregar automáticamente los números de línea. hay algunas herramientas que pueden hacer esto, yo uso una llamada CodeLiner.
Aquí está el código con números de línea, que permitirá que Erl()
funcione correctamente :
Option Compare Database Option Explicit Dim RememberErrNumber As Long Dim RememberErrDescription As String Dim RememberErrSource As String Dim RememberErrLine As Integer Private Sub RememberThenClearTheErrorObject() 10 11 On Error Resume Next 12 " For demo purposes 14 Debug.Print "ERROR RAISED" 15 Debug.Print Err.Number 16 Debug.Print Err.Description 17 Debug.Print Err.Source 18 Debug.Print " " 19 20 " This function has to be declared in the same scope as the variables it refers to 22 RememberErrNumber = Err.Number 23 RememberErrDescription = Err.Description 24 RememberErrSource = Err.Source 25 RememberErrLine = Erl() 26 " Note that the next line will reset the error object to 0, the variables above are used to remember the values " so that the same error can be re-raised 29 Err.Clear 30 " Err.Clear is used to clear the raised exception and set the err object to nothing (ie err.number to 0) " If Err.Clear has not be used, then the next "On Error GoTo ALabel" that is used in this or the procedure that called it " will actually NOT pass execution to the ALabel: label BUT the error is paseed to the procedure that called this procedure. " Using Err.Clear (or "On Error GoTo -1 ") gets around this and facilitates the whole TRY CATCH block scenario I am using there. 35 36 " For demo purposes 38 Debug.Print "ERROR RAISED is now 0 " 39 Debug.Print Err.Number 40 Debug.Print Err.Description 41 Debug.Print Err.Source 42 Debug.Print " " 43 " For demo purposes 45 Debug.Print "REMEMBERED AS" 46 Debug.Print RememberErrNumber 47 Debug.Print RememberErrDescription 48 Debug.Print RememberErrSource 49 Debug.Print " " 50 End Sub Private Sub ClearRememberedErrorObjectValues() 54 " This function has to be declared in the same scope as the variables it refers to 56 RememberErrNumber = 0 57 RememberErrDescription = "" 58 RememberErrSource = "" 59 RememberErrLine = 0 60 End Sub Sub ExampleOfTryCatchBlockInVBA() 67 68 On Error GoTo HandleError 69 70 " ----------------------------------------------------- " SubProcedure1 has the example of a multiple line TRY block with a block of code executed in the event of an error 73 74 SubProcedure1 75 76 77 78 Exit Sub 79 HandleError: 80 81 Select Case Err.Number 82 Case 0 " This shold never happen as this code is an error handler! " However if it does still allow the Err.raise to execute below. (In this case Err.raise will fail " and itself will raise an error "Invalid procedure call or argument" indicating that 0 cannot be used to raise and error! 86 87 Case 111111 " You might want to do special error handling for some predicted error numbers " perhaps resulting in a exit sub with no error or " perhaps using the Err.raise below 91 92 Case Else " Just the Err.raise below is used for all other errors 94 95 End Select 96 " " I include the procedure ManageErrSource as an exmple of how Err.Source can be used to maintain a call stack of procedure names " and store the name of the procedure that FIRST raised the error. " 101 Err.Raise Err.Number _ , ManageErrSource("MyModuleName", Err.Source, Erl(), "tsub1_RaisesProcedureNotFoundError") _ , Err.Number & "-" & Err.Description 104 " Note the next line never gets excuted, but I like to have resume in the code for when I am debugging. " (When a break is active, by moving the next executable line onto it, and using step over, it moves the exection point to the line that actually raised the error) 107 Resume 108 End Sub Sub SubProcedure1() 112 " ----------------------------------------------------- " Example of a multiple line TRY block with a Case statement used to CATCH the error 115 " " It is sometimes better to NOT use this technique but to put the code in it"s own procedure " (ie I refer to the code below that is surrounded by the tag #OWNSUB) . " However,sometimes using this technique makes code more readable or simpler! " 121 122 Dim i As Integer 123 " This line puts in place the defualt error handler found at the very foot of the procedure 125 On Error GoTo HandleError 126 127 " " Perhaps lots of statements and code here " 131 132 " First an example with comments 134 135 " ----------------------------------------------------- " TRY BLOCK START 138 " This next line causes execution to "jump" to the "catch" block in the event an error is detected. 140 On Error GoTo CatchBlock1_Start 141 " #OWNSUB 143 144 tsub_WillNotRaiseError_JustPrintsOk 145 146 If vbYes = MsgBox("1. Do you want to raise an error in the try block? - (PRESS CTRL+BREAK now then choose YES, try no later.)", vbYesNo) Then 147 i = 100 / 0 148 End If 149 " " Perhaps lots of statements and code here " 153 " #OWNSUB 155 " TRY BLOCK END " ----------------------------------------------------- 158 159 " ----------------------------------------------------- " CATCH BLOCK START 162 CatchBlock1_Start: 163 164 If Err.Number = 0 Then 165 On Error GoTo HandleError " Re-instates the procedure"s generic error handler " This is also done later, but I think putting it here reduces the likelyhood of a coder accidentally removing it. 168 169 Else 170 " WARNING: BE VERY CAREFUL with any code that is written here as " the "On Error GoTo CatchBlock1_Start" is still in effect and therefore any errors that get raised could goto this label " and cause and infinite loop. " NOTE that a replacement "On Error Goto" cannot be executed until Err.clear is used, otherwise the "On Error Goto" " will itself raise and error. " THEREFORE KEEP THE CODE HERE VERY SIMPLE! " RememberThenClearTheErrorObject should be the only code executed and this called procedure must be tight! 178 " This saves the details of the error in variables so that the "On Error GoTo HandleError" can be used " to determine how the next Err.Raise used below is handled (and also how any unexpected implicitly raised errors are handled) 181 RememberThenClearTheErrorObject 182 183 On Error GoTo HandleError "#THISLINE# 184 185 If vbYes = MsgBox("2. Do you want to raise an error in the erro handler? - (PRESS CTRL+BREAK now then try both YES and NO )", vbYesNo) Then 186 i = 100 / 0 187 End If 188 189 Select Case RememberErrNumber 190 Case 0: " No Error, do Nothing 191 192 Case 2517 193 Debug.Print "The coder has decided to just give a Warning: Procedure not found " & Err.Number & " - " & Err.Description 194 ClearRememberedErrorObjectValues " Not essential, but might save confusion if coding errors are made 195 196 Case Else " An unexepected error or perhaps an (user) error that needs re-raising occurred and should to be re-raised 198 " NOTE this is giving an example of what woudl happen if the CatchBlock1_ErrorElse is not used below 200 If vbYes = MsgBox("3. Do you want to raise an error in the ELSE error handler? CatchBlock1_ErrorElse *HAS NOT* been used? - (PRESS CTRL+BREAK now then try both YES and NO )", vbYesNo) Then 201 i = 100 / 0 202 End If 203 204 On Error GoTo CatchBlock1_ErrorElse 205 206 " SOME COMPLEX ERROR HANDLING CODE - typically error logging, email, text file, messages etc.. " Because the error objects values have been stored in variables, you can use " code here that might itself raise an error and CHANGE the values of the error object. " You might want to surround the code with the commented out CatchBlock1_ErrorElse lines " to ignore these errors and raise the remembered error. (or if calling a error handling module " just use on error resume next). " Without the CatchBlock1_ErrorElse lines any error raised in this "complex code" will be handled by the " active error handler which was set by the "On Error GoTo HandleError" tagged as "#THISLINE#" above. 215 216 If vbYes = MsgBox("4. Do you want to raise an error in the ELSE error handler when CatchBlock1_ErrorElse HAS been used? - (PRESS CTRL+BREAK now then try both YES and NO )", vbYesNo) Then 217 i = 100 / 0 218 End If 219 220 CatchBlock1_ErrorElse: 221 On Error GoTo HandleError " This line must be preceeded by an new "On error goto" for obvious reasons 223 Err.Raise RememberErrNumber, RememberErrSource, RememberErrDescription 224 225 End Select 226 227 On Error GoTo HandleError 228 229 End If " CATCH BLOCK END " ----------------------------------------------------- 232 On Error GoTo HandleError " Unnecessary but used to delimt the catch block 233 234 235 236 " " lots of code here perhaps " 240 241 242 243 " ----------------------------------------------------- " Example 2 " " In this example goto statements are used instead of the IF statement used in example 1 " and no explanitory comments are given (so you can see how simple it can look) " 250 " ----------------------------------------------------- " TRY BLOCK START 253 254 On Error GoTo CatchBlock2_Start 255 256 tsub_WillNotRaiseError_JustPrintsOk 257 258 If vbYes = MsgBox("Do you want to raise an error? - (PRESS CTRL+BREAK now then choose YES)", vbYesNo) Then 259 i = 100 / 0 260 End If 261 " " Perhaps lots of statements and code here " 265 " TRY BLOCK END " ----------------------------------------------------- 268 269 270 GoTo CatchBlock2_End: 271 CatchBlock2_Start: 272 273 RememberThenClearTheErrorObject 274 275 On Error GoTo HandleError 276 277 Select Case RememberErrNumber 278 Case 0: " No Error, do Nothing 279 280 Case 2517 281 Debug.Print "The coder has decided to just give a Warning: Procedure not found " & Err.Number & " - " & Err.Description 282 ClearRememberedErrorObjectValues " Not essential, but might save confusion if coding errors are made 283 284 Case Else " An unexepected error or perhaps an (user) error that needs re-raising occurred and should to be re-raised " In this case the unexpecetd erro will be handled by teh code that called this procedure " This line must be preceeded by an new "On error goto" for obvious reasons 288 Err.Raise RememberErrNumber, RememberErrSource, RememberErrDescription 289 290 End Select 291 292 On Error GoTo HandleError 293 294 End If 295 296 CatchBlock2_End: " CATCH BLOCK END " ----------------------------------------------------- 299 On Error GoTo HandleError " Unnecessary but used to delimt the catch block 300 301 302 303 " " Here you could add lots of lines of vba statements that use the generic error handling that is after the HandleError: label " " 308 " " You could of course, alway add more TRY CATCH blocks like the above " " 313 314 315 316 Exit Sub 317 HandleError: 318 319 Select Case Err.Number 320 Case 0 " This shold never happen as this code isan error handler! " However if it does still allow the Err.raise to execute below. (In this case Err.raise will fail " and itself will raise an error "Invalid procedure call or argument" indicating that 0 cannot be used to raise and error! 324 325 Case 111111 " You might watch to do special error handling for some predicted error numbers " perhaps exit sub " Perhaps using the Err.raise below 329 End Select 330 " ie Otherwise " " Note that I use the Err.Source to maintain a call stack of procedure names " 335 Err.Raise Err.Number _ , ManageErrSource("MyModuleName", Err.Source, Erl(), "tsub1_RaisesProcedureNotFoundError") _ , Err.Number & "-" & Err.Description 338 " Note the next line never gets excuted, but I like to have resume in the code for when I am debugging. " (By moving the next executable line onto it, and using step over, it moves the exection point to the line that actually raised the error) 341 Resume 342 End Sub Sub tsub_WillNotRaiseError_JustPrintsOk() 348 349 Static i As Integer 350 351 i = i + 1 352 353 Debug.Print "OK " & i 354 End Sub Public Function ManageErrSource(MyClassName As String, ErrSource As String, ErrLine As Integer, ProcedureName As String) As String 360 " This function would normally be in a global error handling module 362 " On Error GoTo err_ManageErrSource 364 365 Const cnstblnRecordCallStack As Boolean = True 366 367 Select Case ErrSource 368 369 Case Application.VBE.ActiveVBProject.Name 370 " Err.Source is set to this value when a VB statement raises and error. eg In Access by defualt it is set to "Database" 372 373 ManageErrSource = Application.VBE.ActiveVBProject.Name & " " & MyClassName & "." & ProcedureName & ":" & ErrLine 374 375 Case "" 376 " When writing code ouside of the error handling code, the coder can raise an error explicitly, often using a user error number. " ie by using err.raise MyUserErrorNumber, "", "My Error descirption". " The error raised by the coder will be handled by an error handler (typically at the foot of a procedure where it was raised), and " it is this handler that calls the ManageErrSource function changing the Err.Source from "" to a meaningful value. 381 382 ManageErrSource = Application.VBE.ActiveVBProject.Name & " " & MyClassName & "." & ProcedureName & ":" & ErrLine 383 384 Case Else 385 " This code is executed when ManageErrSource has already been called. The Err.Source will already have been set to hold the " Details of where the error occurred. " This option can be used to show the call stack, ie the names of the procdures that resulted in the prcedure with the error being called. 389 390 If cnstblnRecordCallStack Then 391 392 If InStr(1, ErrSource, ";") = 0 Then 393 ManageErrSource = ErrSource & ":: Called By: " 394 End If 395 ManageErrSource = ErrSource & ";" & ProcedureName & ":" & ErrLine 396 397 Else 398 ManageErrSource = ErrSource 399 400 End If 401 402 End Select 403 404 Exit Function 405 err_ManageErrSource: 406 Err.Raise Err.Number, "MyModuleName.err_ManageErrSource", Err.Description 407 Resume 408 End Function
Comentarios
- ¡Hola! Bienvenido a Revisión de código. Agregue más contexto a su respuesta: explique por qué su sugerencia mejorará el código OP ‘ s, o tal vez entre en más detalles sobre lo que está tratando de decir.
TypeName(Me)
como fuente de errores personalizados en módulos de clase, y la única forma de que un error sepa qué procedimiento ocurrió en, es codificar el nombre del procedimiento en unaconst
local, idealmente no muy lejos de la firma del método ‘. Me gusta la idea de la pila de llamadas, pero un inconveniente es que debes » presionar » y » pop » cada vez que ingresa / sale de un procedimiento, de lo contrario se convierte en una mentira.