De onderstaande code spreekt voor zich: kopieer en plak het allemaal in een module en voer het uit , biedt het enkele use-cases en veel verklarende opmerkingen in de tekst. (Het werkt, maar ik ben geïnteresseerd om te weten wat andere mensen ervan vinden en voor eventuele suggesties die u zou willen doen.)
De belangrijkste feiten die u zich moet realiseren zijn:
-
Wanneer je op fout goto Label1 gebruikt, komt de procedure in een toestand van “Ik ben bezig met het afhandelen van een fout” als een uitzondering is opgetreden. Als het zich in deze toestand bevindt en een andere “On Error Goto” label2-instructie wordt uitgevoerd, zal deze NIET naar label2 gaan, maar een verhoging en een fout die wordt doorgegeven aan de code die de procedure heeft aangeroepen.
-
U kunt voorkomen dat een procedure zich in de status “Ik” afhandel van een fout “bevindt door de uitzondering te wissen (door err op niets in te stellen zodat de eigenschap err.number 0 wordt) door met
Err.clear or On Error Goto -1 " Which I think is less clear!
(LET OP dat On Error Goto 0
verschilt van het bovenstaande)
Ook belangrijk om op te merken is dat Err.Clear het terugzet naar nul, maar het is eigenlijk gelijk aan:
On Error Goto -1 On Error Goto 0
dwz Err.Clear verwijdert een “On Error Goto” die momenteel aanwezig is. Daarom is het meestal het beste om:
On Error Goto -1
te gebruiken als gebruik van Err.clear. write
Err.Clear On Error Goto MyErrorHandlerLabel
Ik gebruik de bovenstaande technieken met verschillende labels om de soms nuttige functionaliteit te simuleren die Visual basic TRY CATCH-blokken geven, waarvan ik denk dat ze hun plaats in het schrijven hebben leesbare code.
Toegegeven, deze techniek creëert een paar regels code meer dan een mooie VB try catch-instructie, maar het is niet te rommelig en vrij gemakkelijk om je ga rond.
PS. Ook interessant kan de procedure ManageErrSource zijn, die ervoor zorgt dat de eigenschap Err.Source de procedure opslaat waar de fout is opgetreden.
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
Opmerkingen
Answer
Het probleem is dat runtime-fouten in VBA zijn geen uitzonderingen , en foutafhandeling in VBA heeft weinig gemeen met uitzonderingsafhandeling.
RememberErrLine = Erl()
De Erl
functie is een verborgen lid van de VBA.Information
module om een reden – het retourneert 0 tenzij de fout optrad op een genummerde regel. En als je regelnummers in VBA gebruikt, woon je al 25 jaar in een grot en gebruik je waarschijnlijk GoSub
-instructies in plaats van procedures te schrijven. Regelnummers worden ondersteund vanwege legacy / achterwaartse compatibiliteitsredenen , omdat code geschreven in de jaren 80 vereiste hen.
Ik vind het leuk hoe je het zelf zei:
" THEREFORE KEEP THE CODE HERE VERY SIMPLE!
..maar waarom is dat niet van toepassing op de rest van de code? Geen aanstoot, maar dit is spaghettilogica, geschreven in procedures die duidelijk en schaamteloos de Single Responsibility Principle . Geen SRP-compatibele code zou ooit twee van dergelijke ” try-catch ” blokken nodig hebben.
Dit ruikt:
Case 0: " No Error, do Nothing
Het betekent een van twee dingen: ofwel heb je foutafhandelingscode die draait in niet-foutcontexten, of je hebt dode code die moet worden verwijderd.
Dit ruikt stinkt :
GoTo CatchBlock2_End: CatchBlock2_Start:
Ten eerste, ac olon (:
) die geen regellabel specificeert, is een instructiescheidingsteken .Blijkt dat een nieuwe regel ook een ” instructiescheidingsteken ” is, dus de dubbele punt aan het einde van GoTo CatchBlock2_End
is volkomen nutteloos en verwarrend, vooral gezien het inspringingsniveau van de GoTo-instructie .
Over GoTo
…
Neil Stephenson vindt het schattig om zijn labels” dengo “te noemen
Ik vind het niet leuk hoe ik tussen labels moet springen om de code te volgen. IMO het is rommelig en nodeloos gespagettificeerd.
Prima, smartypants . Dus, hoe ga je dan netjes om met fouten in VBA?
1. Schrijf in de eerste plaats zuivere code.
Houd u aan de beste praktijken en schrijf kleine procedures die één ding doen, en doe het ook goed.
2. Schrijf objectgeoriënteerde code.
Abstractie en inkapseling zijn twee van de 4 pijlers van OOP , en ze “worden volledig ondersteund in VBA. Polymorfisme is ook enigszins een optie; alleen de juiste overerving is uitgesloten, maar dat belet niet dat concepten in klassemodules en het instantiëren van gespecialiseerde objecten.
Procedurele code geschreven in standaardmodules (.bas) zou kleine, kleine openbare methoden moeten zijn (macro ” hooks “) die de objecten creëren die nodig zijn om de functionaliteit uit te voeren.
Dus, hoe verhoudt dat zich zelfs op afstand tot de juiste foutafhandeling?
3. Omarm idiomatische foutafhandeling, vecht er niet tegen.
Gegeven code die aan de bovenstaande punten voldoet, is er geen reden om de foutafhandeling niet te implementeren de idiomatische VBA-manier.
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
Dit patroon is analoog aan een ” try-catch-eindelijk ” op de volgende manier:
- De body is de ” probeer ” deel, dat doet wat de naam van de methode zegt en niets meer
-
CleanFail
is het ” catch ” deel, dat alleen wordt uitgevoerd als er een fout optreedt -
CleanExit
is het ” tot slot ” deel, dat wordt uitgevoerd ongeacht of er een fout is opgetreden … tenzij je “opnieuw opnieuw gooit . Maar als je dan een fout moet opblazen om de aanroepcode af te handelen, zou je niet veel opruimen moeten hebben c ode om uit te voeren, en je zou een heel erg goede reden moeten hebben om dit te doen.
Als je foutafhandelingssubroutine een fout kan veroorzaken, dan houd je je niet aan SRP. Het schrijven naar een logboekbestand is bijvoorbeeld een probleem op zich, dat moet worden samengevat in een Logger
-object dat leeft om logboekproblemen op te lossen en methoden blootlegt die hun eigen fouten afhandelen . Foutafhandeling van subroutinecode moet triviaal zijn.
Reacties
- Bedankt @mat ‘ zelfvoldaan de tijd om opmerkingen toe te voegen die me echt hebben geholpen ‘ m op te nemen voor gewelddadige maar humoristische kritiek. Ik ‘ heb mijn code bekeken en ‘ ben blij te kunnen zeggen dat de overgrote meerderheid zich houdt aan de principes die u beschrijft. Uw uitleg was echter nuttig en het deed me nadenken en me realiseerden dat ik ‘ niet inzag dat VB- en SQL Server TRY CATCH-statements slechts één keer in elke procedure worden gebruikt (ik dacht dat ze een betekent dat je geen code hoeft te abstraheren om het leesbaarder te maken). Als je zin hebt om wat meer opmerkingen over de ManageErrSource-procedure toe te voegen, ‘ m alle oren …
- @HarveyFrench I ‘ Ik zal er nog wat toevoegen als ik de kans krijg – hadn ‘ t hiernaar gekeken 😉 Het verwijzen naar en het gebruik van de VBIDE API vereist speciale beveiligingsinstellingen, wat niet cool is. Ik ‘ ben
TypeName(Me)
gaan gebruiken als bron voor aangepaste fouten in klassemodules, en de enige manier voor een fout om te weten welke procedure het gebeurde in, is om de procedurenaam hard te coderen in een lokaleconst
, idealiter niet te ver van de methode ‘ s handtekening. Ik vind het idee van de call-stack leuk, maar een nadeel is dat je consequent ” moet pushen ” en ” pop ” telkens wanneer u een procedure opent / verlaat, anders wordt het een leugen. - De code die ik van fmsinc.com heb ontvangen, verspreidt zich vaak van de problemen die ik ‘ heb ondervonden.Ik ‘ waardeer je mening. Kijk hier codereview.stackexchange.com/questions/94498/… Ik waardeer uw tijd aangezien dit mij drijft nuts.
Answer
Luister naar Mat “sMug , maar hij behandelde niet de situatie waarin u echt weet hoe u een fout kunt herstellen. Voor de volledigheid “wil ik dat graag even behandelen.
Laten we eerst eens kijken hoe we zoiets in VB.Net zouden doen.
Try foo = SomeMethodLikelyToThrowAnException Catch e As SomeException foo = someDefaultValue End Try " some more code
De idiomatische manier om dit te doen in VB6 is door ResumeNext
. Schrijf dit op, want het is de only keer dat ik het ooit zal zeggen recht op 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
De alternatieve manier is om deze logica in lijn te brengen, die volgens mij een beetje schoner is en dichter bij het Try...Catch
idioom, maar het kan lelijk snel worden als het wordt misbruikt.
On Error Resume Next foo = SomeMethodLikelyToRaiseAnError If Err.Number = ConstantValueForErrorWeExpected Then foo = someDefaultValue End If On Error Goto 0
Beide is een idiomatische manier om met verwachte fouten om te gaan, maar wat je ook doet. Doe geen moeite met Resume Next
totdat je volledig begrijpt wat het doet en wanneer het gepast is . (Meer een waarschuwing voor toekomstige lezers dan voor jou. Je lijkt het afhandelen van fouten in VB6 grondig te begrijpen. Misschien iets te goed voor je eigen bestwil.)
Opmerkingen
- Bedankt @RubberDuck voor je nuttige opmerkingen. Eerlijk gezegd merk ik dat ik ” gebruik Bij fout hervatten volgende ” eerder nogal wat procedure-oproepen afte r waarbij er meestal een SELECT CASE is die reageert op elke opgetreden fout. De grote fout die ik besef dat ik maak, is dat ik een door de gebruiker gedefinieerde uitzondering opwerp in de subprocedure om situaties te markeren die zich voordoen (zoals de gebruiker die vraagt om de verwerking te annuleren). Ik denk dat ik meer functies moet gebruiken. Dit is een indicatie dat mijn algemene codestructuur ” niet ideaal ” / slecht is en ik denk dat ik dit moet aanpakken. Bedankt.
- Je ‘ hebt een geweldig punt bereikt @HarveyFrench. Uitzonderingen zijn voor uitzonderlijk gedrag, niet voor controlestroom. Welkom bij CR.
- Ik ‘ zou erg geïnteresseerd zijn in uw mening over deze SO-vraag: stackoverflow. com / questions / 31007009 / …
- De code die ik van fmsinc.com heb ontvangen, lost veel van de problemen op die ik ‘ hebben gehad. Ik ‘ waardeer je mening. Kijk hier codereview.stackexchange.com/questions/94498/…
Answer
Dit antwoord is bedoeld om het Try / Catch-patroon te vereenvoudigen zodat het gemakkelijk te begrijpen is.
Dit verschilt niet veel van reguliere inline foutafhandeling, behalve dat het meerdere regels tegelijk kan overslaan, een fout kan afhandelen en vervolgens de normale uitvoering kan hervatten. Dit is een zeer netjes gestructureerd patroon voor het afhandelen van een fout. De stroom beweegt zeer zuiver van boven naar beneden; geen spaghetticode hier.
Traditioneel wordt de foutafhandelaar onderaan geplaatst. Maar het Try / Catch-construct is zo elegant. Het is een zeer gestructureerde manier om met fouten om te gaan en is heel gemakkelijk te volgen. Dit patroon probeert dat op een zeer duidelijke, beknopte manier weer te geven. De stroom is zeer consistent en springt niet van plaats naar plaats.
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
Bronnen:
- Pearson-foutafhandeling in VBA
- Procedure: Runtime-fouten in VBA afhandelen
- Juiste afhandeling Fouten in VBA (Excel)
- Mijn eigen: hoe een inline foutafhandelingsblok zoals Try / Catch
Goed beheerd, dit werkt best goed. Het is een zeer schoon vloeiend patroon dat overal waar het nodig is reproduceerbaar is.
Reacties
- @D_Bester, Bedankt voor de links en het eenvoudige voorbeeld. Ik ‘ ben nog aan het leren en vond je feedback nuttig, maar je moet een ” toevoegen bij fout Ga naar 0 ” na de ” op Fout goto -1 “. Ook bij nader inzien denk ik dat het beter is om Err.Clear te gebruiken in plaats van ” On Error Goto -1 ” omdat het duidelijker laat zien wat is aan het gebeuren. Ik ‘ vind deze hele foutafhandeling in VBA een beetje een zwarte kunst.
- @D_Bester. Bij nader inzien is uw code prima als u de gebruiker alleen een bericht wilt geven wanneer er een fout optreedt, maar wat als u de fout opnieuw wilt verhogen? Dat zal een veel voorkomend scenario zijn. Overwegen.Als je code probeerde om de details van een klant ‘ s op te zoeken en het ‘ niet kon krijgen om een ONVERWACHTE reden. U zou de fout opnieuw moeten verhogen en de code die uw code gebruikt om de zoekactie uit te voeren, laten beslissen wat u moet doen.
- @HarveyFrench Als u de fout opnieuw wilt verhogen, gebruikt u gewoon ‘ Err.Raise ‘. Geen probleem, aangenomen dat de code goed is gestructureerd en foutafhandeling is ingeschakeld in de aanroepcode.
- @HarveyFrench
Err.Clear
enOn Error Goto -1
zijn NIET equivalent. Zie stackoverflow.com/a/30994055/2559297 - U ‘ heeft gelijk dat ze zijn niet hetzelfde sorry. Maar ik denk dat de bovenstaande code nog steeds On Error GoTo -1 moet vervangen door Err.Clear, anders de ” ‘ meer code zonder foutafhandeling ” springt naar ErrHandler1 als er een fout optreedt.
Answer
Betreffende “CleanExit” en het “Eindelijk” onderwerp.
Mat “s Mug schreef:
CleanExit is de ” eindelijk ” deel, dat wordt uitgevoerd ongeacht of er een fout is opgetreden … tenzij je “opnieuw gooit.
Een dergelijke situatie kan zich bijvoorbeeld voordoen in deze procedurecode:
Procedurele aanpak
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
Probleem hier : Als er een fout optreedt in de body van de methode die opnieuw moet worden gegenereerd in CleanFail, wordt CleanExit niet uitgevoerd. en daarom kan de bron “niet correct worden gesloten.
Natuurlijk, je zou de bron ook in de fout-handler zelf kunnen sluiten, maar dat zou kunnen leiden tot meerdere codefragmenten waar de bronafhandeling zal zijn / heeft te doen.
Mijn suggestie is om een aangepast object te gebruiken voor elke resource-binding-noodzaak:
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
Objectgeoriënteerde benadering
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
Mogelijkheid : Omdat het aangepaste object buiten het bereik valt nadat de fout is opgetreden, wordt de Terminate-methode automatisch uitgevoerd, wat ervoor zorgt dat de opgehaalde bron correct wordt gesloten / opgeruimd.
Een noodzaak minder voor een “eindelijk” blok.
Foutafhandeling in Terminate-methode
Naar mijn mening is het contextafhankelijk hoe een fout zal worden afgehandeld in de Terminate-methode van de aangepaste klasse. Misschien moet het ergens extra worden geregistreerd of zelfs helemaal worden ingeslikt?
Dit is zeker bespreekbaar.
Maar het is essentieel voor schakel een fout-handler in deze methode in, omdat, voor zover ik weet, elke niet-afgehandelde fout in deze methode ervoor zorgt dat VBA de uitvoering verbreekt en het standaard runtime-foutberichtvenster weergeeft.
Antwoord
Om mijn vorige bericht te verduidelijken, de volgende regel uit de code van HarveyFrench:
RememberErrLine = Erl()
werkt niet tenzij regelnummers zijn toegevoegd aan elke regel code. In plaats van handmatig regelnummers te typen, wat veel te vervelend is, kunt u een tool gebruiken om de regelnummers automatisch toe te voegen. Daar zijn er een paar tools die dit kunnen doen, ik gebruik er een genaamd CodeLiner.
Hier is de code met regelnummers, waarmee Erl()
succesvol kan werken :
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
Reacties
- Hallo! Welkom bij Code Review. Voeg alstublieft meer context toe aan uw antwoord: leg uit waarom uw suggestie de code van OP ‘ s zal verbeteren, of ga misschien meer in op wat u probeert te zeggen.
On Error Goto -1
On Error Resume Next
enOn Error GoTo 0
en controleer deErr.Number
. Het bovenstaande is wat moeilijk te volgen, heeft een beetje eenspaghetti
structuur ..