I ran into an interesting dilemma today. I have a function that handles information and checks for duplicate values, then returns the next number that is not a duplicate. So, I have something like this:
Public Function GetNextNonDuplicateNumber(NumberToCheck as Long) as Long
//the non-duplicate the function will return
Dim NonDuplicate as Long
If CheckForDuplicate(NumberToCheck) = True Then
Throw New DuplicateException()
Else
NonDuplicate = NumberToCheck
End If
End Function
Then at the bottom of the function I have a catch block that handles the duplicate by incrementing until I don't have a duplicate any more, like this:
Catch ex as DuplicateException
NonDuplicate = IncrementToNonDuplicateValue(NumberToCheck)
Throw ex
Return NonDuplicate
End Function
As you can see, I want to handle the exception specifically, but I also want to throw it when I'm done because I want to alert other code outside the function.
The problem is that simply throwing it exits out of the function with a null value. Am I thinking about a try/catch the wrong way, or is there a way around this?
If you caught an exception and recovered from it (with your IncrementToNonDuplicate...) then there is no reason to throw an exception anymore. Code between catch and end try should just clean the resources like closing a file or datareader if you will rethrow it.
You could rather return a structure that contains NonDuplicate value and required information about errors in function.
Other way would be to throw a custom exception that will contain information like "Invalid number: it should be...)
You can return a boolean indicating if a duplicate is found, and change the parameter to be passed in by reference so you can update the value.
Public Function GetNextNonDuplicateNumber(ByRef NonDupeNumber as Long) as Boolean
Related
I have an async function that makes a call to an API, but sometimes there is bad data and I want an exception to be thrown to stop other subsequent procedures from having to run. The Async procedure looks like this:
public async Function getInfo(url as string) as task(of string)
Dim htpRes As HttpResponseMessage = Await url.GetAsync().ConfigureAwait(False)
Dim result = htpRes.Content.ReadAsStringAsync.Result
If result = "" Then
Throw New Exception("API Failed")
Else
Return result
End If
End Function
That function is called by a procedure that looks like this:
sub hitAllAPIs(apiList As List(Of String))
For each i In apiList
Try
Dim info As String = getInfo(i)
doOtherStuffWithInfo(info)
Catch ex As Exception
logError
End Try
Next
End sub
The desired behavior is for the forloop in 'hitAllAPIs' to keep running even if an exception is thrown within 'getInfo'. Instead, what happens is that the exception gets hit and stops the code from running, whether I'm in Debug mode or Release mode. If I'm not there to babysit it and hit 'continue' then the forloop will just stop and the program won't run anymore. Once I hit 'continue', btw, the 'Catch' will work and the error will be logged.
The issue is that I need this to all happen automatically and that's not happening. I can't just eliminate the exception and check the function for a null value, since this is a very simplified version of my code and the function is actually called all over the place. I know that I can change my exception settings to simply skip over all exceptions like this, but this is happening even in Release mode to code that has been deployed. I can't imagine that my debugging exceptions should have an effect on code deployed in Release mode. In any case, I'm hoping someone can help me understand why this exception isn't being automatically handled by the try block.
Thanks!
It seems result = "" is an expected result not an exception. Using Try/Catch is rather heavy handed. Exception handling is for unexpected results. Get rid of the Throw in the Function and add and If in the For Each.
Public Async Function getInfo(url As String) As Task(Of String)
Dim htpRes As HttpResponseMessage = Await url.GetAsync().ConfigureAwait(False)
Dim result = htpRes.Content.ReadAsStringAsync.Result
Return result
End Function
Sub hitAllAPIs(apiList As List(Of String))
For Each i In apiList
Dim info As String = getInfo(i)
If info = "" Then
'Add an overload of logError that accepts a string
logError("API failed")
Else
doOtherStuffWithInfo(info)
End If
Next
End Sub
I have a simple block of legacy code that sits inside of a loop that crawls through some potentially bad xml node by node and which needs to be refactored as it is not working as intended:
Try
xmlFrag.LoadXml("<temproot>" & strXMLfragment & "</temproot>")
writer.WriteRaw(strXMLfragment)
Catch ex As Exception
InvalidXML = True
End Try
What this block is meant to do is check for valid xml and then write the xml out. What it actually does is check for invalid xml and then write the xml out only if it is valid. So it needs to be fixed to work as intended.
My first attempt at a fix:
Try
xmlFrag.LoadXml("<temproot>" & strXMLfragment & "</temproot>")
'writer.WriteRaw(strXMLfragment)
Catch ex As Exception
InvalidXML = True
Finally
writer.WriteRaw(strXMLfragment)
End Try
This works on my test data but I am concerned that WriteRaw may throw an exception on other data. I haven't found a conclusive statement about what will cause WriteRaw to throw an exception and what will happen when code in a Finally block throws an exception.
So I tried rewriting it like this:
Try
xmlFrag.LoadXml("<temproot>" & strXMLfragment & "</temproot>")
Catch ex As Exception
InvalidXML = True
End Try
Try
writer.WriteRaw(strXMLfragment)
Catch
End Try
Frankly it looks ugly as hell. Is there a more elegant way to refactor this or will the first attempt be suitable?
When an excpetion is raised in a Finally block, nothing special happens: the exception is propagated out and up like any other exception, and code after the exception in this finally block will not be executed.
Your first attempt will fail if strXMLfragment is null or an empty string (or due to a already running asynchronous operation).
So if you really want to handle/swallow all exceptions, you'll have to use two Try blocks.
To make it cleaner, you might want to pull your first Try/Catch into it's own private function and make it reusable:
Private Function TryParseXml(ByVal xml as String) as Boolean
Try
XDocument.Parse(xml)
Return True
Catch ex As Exception
Return False
End Try
End Function
Then wrap your writer.WriteRaw call in it's own Try/Catch.
Dim myXml = "<temproot>" & strXMLfragment & "</temproot>"
If TryParseXml(myXml) Then
xmlFrag.LoadXml(myXml)
Else
Try
writer.WriteRaw(strXMLfragment)
Catch ex as Exception
' handle exception
End Try
End If
Yes, ultimately this is using two Try/Catch blocks. There is no real way around this as the only real way to determine if Xml is valid is to attempt to parse it and wait for it to blow up.
In the end I came up with this profoundly simple and elegant refactoring:
Try
writer.WriteRaw(strXMLfragment)
xmlFrag.LoadXml("<temproot>" & strXMLfragment & "</temproot>")
Catch ex As Exception
InvalidXML = True
End Try
With the WriteRaw line executing first it will always write out the XML unless it throws an exception. Then the LoadXml line can test for validity without interfering with writing the xml out. This way the InvalidXML flag is set as designed and there won't be any unexpected exceptions.
Here's the code example :
Try
Throw New FirstException()
Finally
Throw New SecondException()
End Try
I figured out it only throws SecondException out and FirstException just vanishes.
I thought FirstException would be inside InnerException property of SecondException but it appears it is not.
I'm not blocked on anything as I don't really need the FirstException to show up, I'm just rather intrigued about this behaviour.
Is there a way to know SecondException did get thrown first when
catching it all at upper level ?
If the first exception really is overriden by the second, what is the
reason ?
Does it happen in every other language ? Is it logical ?
I guess the primary explanation for why this works this way is that you are never catching your first exception and passing it along the chain. If you have a situation like the above where you may be throwing several exceptions on the way back to the original caller then you have to either catch them as they are thrown (and include them as an inner exception when creating the next one) :
Dim ex1 As Exception = Nothing
Try
Throw New Exception("first exception")
Catch ex As Exception
ex1 = ex
Finally
Throw New Exception("second exception", ex1)
End Try
Or, probably better - just don't throw until you have all of the exceptions figured out:
Dim ex1 As Exception = Nothing
Try
ex1 = New Exception("first exception")
Finally
Throw New Exception("second exception", ex1)
End Try
Throwing and catching exceptions is expensive, so it's probably best to not throw until you're ready to return and just log along the way.
One of the limitations of exception handling in .net is that there is no nice way for code in a Finally block to know what exception, if any, caused the code in the Try block to exit, nor is there any normal way for code in a finally block which does have such information to make it available to code which might throw an exception.
In vb.net, it's possible to kludge things in a manner that works pretty well, even though it looks a bit ugly.
Module ExceptionDemo
Function CopySecondArgToFirstAndReturnFalse(Of T)(ByRef dest As T, src As T) As Boolean
dest = src
Return False
End Function
Function AnnotateExceptionAndReturnFalse(ex As Exception, TryBlockException As Exception) As Boolean
If ex Is Nothing Then Return False ' Should never occur
If TryBlockException Is Nothing Then Return False ' No annotation is required
ex.Data("TryBlockException") = TryBlockException
Return False
End Function
Sub ExceptionTest(MainAction As Action, CleanupAction As Action)
Dim TryBlockException As Exception = Nothing
Try
MainAction()
Catch ex As Exception When CopySecondArgToFirstAndReturnFalse(TryBlockException, ex)
' This block never executes, but above grabs a ref to any exception that occurs
Finally
Try
CleanupAction()
Catch ex As Exception When AnnotateExceptionAndReturnFalse(ex, TryBlockException)
' This block never executes, but above performs necessary annotations
End Try
End Try
End Sub
Sub ExceptionTest2(Message As String, MainAction As Action, CleanupAction As Action)
Debug.Print("Exception test: {0}", Message)
Try
ExceptionTest(MainAction, CleanupAction)
Catch ex As Exception
Dim TryBlockException As Exception = Nothing
Debug.Print("Exception occurred:{0}", ex.ToString)
If ex.Data.Contains("TryBlockException") Then TryBlockException = TryCast(ex.Data("TryBlockException"), Exception)
If TryBlockException IsNot Nothing Then Debug.Print("TryBlockException was:{0}", TryBlockException.ToString)
End Try
Debug.Print("End test: {0}", Message)
End Sub
Sub ExceptionDemo()
Dim SuccessfulAction As Action = Sub()
Debug.Print("Successful action")
End Sub
Dim SuccessfulCleanup As Action = Sub()
Debug.Print("Cleanup is successful")
End Sub
Dim ThrowingAction As Action = Sub()
Debug.Print("Throwing in action")
Throw New InvalidOperationException("Can't make two plus two equal seven")
End Sub
Dim ThrowingCleanup As Action = Sub()
Debug.Print("Throwing in cleanup")
Throw New ArgumentException("That's not an argument--that's just contradiction")
End Sub
ExceptionTest2("Non-exception case", SuccessfulAction, SuccessfulCleanup)
ExceptionTest2("Exception in main; none in cleanup", ThrowingAction, SuccessfulCleanup)
ExceptionTest2("Exception in cleanup only", SuccessfulAction, ThrowingCleanup)
ExceptionTest2("Exception in main and cleanup", ThrowingAction, ThrowingCleanup)
End Sub
End Module
The module above starts with a couple helper modules which should probably be in their own "Exception helpers" module. The ExceptionTest method shows the pattern for code which might throw an exception in both the Try and Finally block. The ExceptionTest2 method calls ExceptionTest and reports what exception if any comes back from it. ExceptionDemo calls ExceptionTest2 in such a way as to cause exceptions in different combinations of the Try and Finally blocks.
As shown, if an exception occurs during cleanup, that exception will be returned to the caller, with the original exception being an item in its Data dictionary. An alternative pattern would be to catch the exception that occurs on cleanup and include it in the data of the original exception (which would be left uncaught). My general inclination is that it's probably better in many cases to propagate the exception that occurs during cleanup, since any code which was planning to deal with the original exception will probably expect that cleanup succeeded; if such an expectation cannot be met, the exception that escapes should probably not be the one the caller was expecting. Note also that the latter approach would require a slightly different method of adding information to the original exception, since an exception which is thrown in a nested Try block might need to hold information about multiple exceptions that were thrown in nested Finally blocks.
I wish to run a unit test on a particular dictionary in my code, trying to get a value I don't expect to be in the database (in this case, key=1).
I have written the following code:
Try
Dim s As String = myDict(1)
Catch ex As KeyNotFoundException
Assert.AreEqual("The given key was not present in the dictionary.", ex.Message)
Catch ex As Exception
Assert.Fail()
Throw
End Try
which works fine, but the code analysis is complaining about the "Dim s as String" declaration, as it says that s will never be used for anything. Well that's intentional, because I intend for this to throw an exception and s is irrelevant.
However, I can't seem to find a way to eliminate s from the code. Simply removing the assignment:
Try
myDict(1)
Catch ex As KeyNotFoundException
Assert.AreEqual("The given key was not present in the dictionary.", ex.Message)
Catch ex As Exception
Assert.Fail()
Throw
End Try
now fails to compile. Any suggestions on how to do this?
Unfortunately there is really no way to fix this in typed code. The call myDict(1) is an indexer and it's not legal as a statement (also illegal in C#). In order to test this you will need to use this expression as a part of legal statement.
One way to accomplish this is pass the value as a parameter to a method which doesn't use it
Sub Unused(ByVal o As Object)
End Sub
...
Unused(myDict(1))
if you are using NUnit Framework than
you could use the following code
Dim f As Func(Of Integer, String) = Function(i) myDict.Item(i)
Dim a As TestDelegate = Function() f(1)
Dim ex As KeyNotFoundException = Assert.Throws(Of KeyNotFoundException)(a)
Assert.AreEqual("The given key was not present in the dictionary.", ex.Message)
This is a similar solution which is proposed by JaredPar
Another option is to make the test returning a value and use the ExpectedException attribute so the code could look like this:
<TestCase(New Object(0 - 1) {}, Result:=Nothing), ExpectedException(GetType(KeyNotFoundException), ExpectedMessage:="The given key was not present in the dictionary."), Test> _
Public Function MyTest() As String
Return myDict.Item(1)
End Function
Looks like I can do this by putting a line after the dictionary call which uses the s variable:
Try
Dim s As String = theDocumentsWithUserNameDictDto.Dict(1)
Assert.Fail("Found unexpected value for dictionary key 1: " & s)
Catch ex As KeyNotFoundException
Assert.AreEqual("The given key was not present in the dictionary.", ex.Message)
End Try
I still don't expect the variable to be used (if the test passes), but this does have the benefit of providing extra clarity to the user if the test does fail for some reason.
What is the convention in VB when a sub requires that a try/catch block succeed in order to function, but the catch block doesn't bubble the exception up?
I could put all of the code into the try block, but this seems messy since most of it doesn't need to be tried, it just needs the try to have succeeded.
For example, should the catch block exit the sub? This would work in my current situation, and if it is the proper procedure, let me know, but what about the more general scenario where both both success and failure require additional processing?
I would so something along the lines of
Dim success As Boolean = False
Try
'Code to execute
success = True
Catch ex As Exception
End Try
If success Then
'success processing
Else
'failure processing
End If
This is an unanswered old question, so I try to answer it perhaps can help someone else.
Try this:
Dim successState As Boolean = True
Try
' Do something in here that
' might raise an error.
Catch
' Handle exceptions that occur within
' the Try block, here.
successState = False
Finally
' Perform cleanup code in here.
End Try
If successState Then
MessageBox.Show("Success!")
End If
When it catch error, no success box will appear.