Handling errors in math functions

2019-01-07 22:14发布

问题:

What is good practice for error handling in math-related functions? I'm building up a library (module) of specialized functions and my main purpose is to make debugging easier for the code calling these functions -- not to make a shiny user-friendly error handling facility.

Below is a simple example in VBA, but I'm interested in hearing from other languages as well. I'm not quite sure where I should be returning an error message/status/flag. As an extra argument?

Function AddArrays(arr1, arr2)
    Dim i As Long
    Dim result As Variant

    ' Some error trapping code here, e.g.
    ' - Are input arrays of same size?
    ' - Are input arrays numeric? (can't add strings, objects...)
    ' - Etc.

    ' If no errors found, do the actual work...
    ReDim result(LBound(arr1) To UBound(arr1))
    For i = LBound(arr1) To UBound(arr1)
        result(i) = arr1(i) + arr2(i)
    Next i

    AddArrays = result
End Function

or something like the following. The function returns a boolean "success" flag (as in the example below, which would return False if the input arrays weren't numeric etc.), or an error number/message of some other type.

Function AddArrays(arr1, arr2, result) As Boolean

    ' same code as above

    AddArrays = booSuccess

End Function

However I'm not too crazy about this, since it ruins the nice and readable calling syntax, i.e. can't say c = AddArrays(a,b) anymore.

I'm open to suggestions!

回答1:

Obviously error handling in general is a big topic, and what the best practice is depends a lot on the capabilities of the language you're working with and how the routine you're coding fits in with other routines. So I'll constrain my answer to VBA (used within Excel) and library-type routines of the sort you're describing.

Exceptions vs. Error Codes in Library Routines

In this case, I would not use a return code. VBA supports a form of exception handling that, while not as powerful as the more standard form found in C++/Java/??.NET, is pretty similar. So the advice from those languages generally applies. You use exceptions to tell calling routines that the called routine can't do it's job for whatever reason. You handle exceptions at the lowest level where you can do something meaningful about that failue.

Bjarne Stroustrup gives a very good explanation of why exceptions are better than error codes for this kind of situation in this book. (The book is about C++, but the principles behind C++ exception handling and VBA error handling are the same.)

http://www2.research.att.com/~bs/3rd.html

Here is a nice excerpt from Section 8.3:

When a program is composed of separate modules, and especially when those modules come from separately developed libraries, error handling needs to be separated into two distinct parts: [1] The reporting of error conditions that cannot be resolved locally [2] The handling of errors detected elsewhere The author of a library can detect runtime errors but does not in general have any idea what to do about them. The user of a library may know how to cope with such errors but cannot detect them – or else they would be handled in the user’s code and not left for the library to find.

Sections 14.1 and 14.9 also address exceptions vs. error codes in a library context. (There is a copy of the book online at archive.org.)

There is probably lots more about this on stackoverflow. I just found this, for example:

Exception vs. error-code vs. assert

(There can be pitfalls involving proper management of resources that must be cleaned up when using exceptions, but they don't really apply here.)

Exceptions in VBA

Here is how raising an exception looks in VBA (although the VBA terminology is "raising an error"):

Function AddArrays(arr1, arr2) 
    Dim i As Long 
    Dim result As Variant 

    ' Some error finding code here, e.g. 
    ' - Are input arrays of same size? 
    ' - Are input arrays numeric? (can't add strings, objects...) 
    ' - Etc. 

    'Assume errorsFound is a variable you populated above...
    If errorsFound Then
        Call Err.Raise(SOME_BAD_INPUT_CONSTANT)    'See help about the VBA Err object. (SOME_BAD_INPUT_CONSTANT is something you would have defined.)
    End If

    ' If no errors found, do the actual work... 
    ReDim result(LBound(arr1) To UBound(arr1)) 
    For i = LBound(arr1) To UBound(arr1) 
        result(i) = arr1(i) + arr2(i) 
    Next i 

    AddArrays = result 
End Function

If this routine doesn't catch the error, VBA will give other routines above it in the call stack a chance to (See this: VBA Error "Bubble Up"). Here is how a caller might do so:

Public Function addExcelArrays(a1, a2)
    On Error Goto EH

    addExcelArrays = AddArrays(a1, a2)

    Exit Function

EH:

    'ERR_VBA_TYPE_MISMATCH isn't defined by VBA, but it's value is 13...
    If Err.Number = SOME_BAD_INPUT_CONSTANT Or Err.Number = ERR_VBA_TYPE_MISMATCH Then

        'We expected this might happen every so often...
        addExcelArrays = CVErr(xlErrValue)
    Else

        'We don't know what happened...
        Call debugAlertUnexpectedError()    'This is something you would have defined
    End If
End Function

What "do something meaningful" means depends on the context of your application. In the case of my caller example above, it decides that some errors should be handled by returning an error value that Excel can put in a worksheet cell, while others require a nasty alert. (Here's where the case of VBA within Excel is actually not a bad specific example, because lots of applications make a distinction between internal and external routines, and between exceptions you expect to be able to handle and error conditions that you just want to know about but for which you have no response.)

Don't Forget Assertions

Because you mentioned debugging, it's also worth noting the role of assertions. If you expect AddArrays to only ever be called by routines that have actually created their own arrays or otherwise verified they are using arrays, you might do this:

Function AddArrays(arr1, arr2) 
    Dim i As Long 
    Dim result As Variant 

    Debug.Assert IsArray(arr1)
    Debug.Assert IsArray(arr2)

    'rest of code...
End Function

A fantastic discussion of the difference between assertions and exceptions is here:

Debug.Assert vs Exception Throwing

I gave an example here:

Is assert evil?

Some VBA Advice About General Array Handling Routines

Finally, as a VBA-specific note, there are VBA variants and arrays come with a number of pitfalls that must be avoided when you're trying to write general library routines. Arrays might have more than one dimension, their elements might be objects or other arrays, their start and end indices might be anything, etc. Here is an example (untested and not trying to be exhaustive) that accounts for some of that:

'NOTE: This has not been tested and isn't necessarily exhaustive! It's just
'an example!
Function addArrays(arr1, arr2)

    'Note use of some other library functions you might have...
    '* isVect(v) returns True only if v is an array of one and only one
    '  dimension
    '* lengthOfArr(v) returns the size of an array in the first dimension
    '* check(condition, errNum) raises an error with Err.Number = errNum if
    '  condition is False

    'Assert stuff that you assume your caller (which is part of your
    'application) has already done - i.e. you assume the caller created
    'the inputs, or has already dealt with grossly-malformed inputs
    Debug.Assert isVect(arr1)
    Debug.Assert isVect(arr2)
    Debug.Assert lengthOfArr(arr1) = lengthOfArr(arr2)
    Debug.Assert lengthOfArr(arr1) > 0

    'Account for VBA array index flexibility hell...

    ReDim result(1 To lengthOfArr(arr1)) As Double
    Dim indResult As Long

    Dim ind1 As Long
    ind1 = LBound(arr1)

    Dim ind2 As Long
    ind2 = LBound(arr2)

    Dim v1
    Dim v2

    For indResult = 1 To lengthOfArr(arr1)

        'Note implicit coercion of ranges to values. Note that VBA will raise
        'an error if an object with no default property is assigned to a
        'variant.
        v1 = arr1(ind1)
        v2 = arr2(ind2)

        'Raise errors if we have any non-numbers. (Don't count a string
        'with numeric text as a number).
        Call check(IsNumeric(v1) And VarType(v1) <> vbString, xlErrValue)
        Call check(IsNumeric(v2) And VarType(v2) <> vbString, xlErrValue)

        'Now we don't expect this to raise errors.
        result(indResult) = v1 + v2

        ind1 = ind1 + 1
        ind2 = ind2 + 1
    Next indResult

    addArrays = result
End Function


回答2:

First, PowerUser gave you a good answer already--this is an expansion on that one.

A trick that I just learned is the "double resume", thus:

Function Foobar (Arg1, Arg2)
On Error goto EH
   Do stuff

FuncExit:
  Exit Function
EH:
   msgbox "Error" & Err.Description
   Resume FuncExit
   Resume
End Function 

What happens here is that in the execution of finished code, your code throws up a MsgBox when an error is encountered, then runs the Exit Function statement & goes on its way (just the same as dropping out the bottom with End Function). However, when you're debugging and you get that MsgBox you instead do a manual Ctrl-Break, then set next statement (Ctrl-F9) to the unadorned Resume and press F8 to step--it goes right back to the line that threw the error. You don't even have to take the extra Resume statements out, since they will never execute without manual intervention.

The other point on which I want to argue (gently) with PowerUser is in the final example. I think it's best to avoid unneeded GoTo statements. A better approach is If intvar<=2 then err.raise SomeCustomNumber. Make sure you use a number that isn't already in use--search 'VB custom error' for more information.



回答3:

There's lots of ways to trap errors, some better than others. Alot of it depends on on the nature of the error and how you want to handle it.

1st: In your examples, you aren't handling the basic compiling & runtime errors (see code below).

Function Foobar (Arg1, Arg2)
     On Error goto EH
     Do stuff
     Exit Function
EH:
     msgbox "Error" & Err.Description
End Function

2nd: Using the framework example above, you can add all the if-then logical error trapping statements you want & feed it to the EH step. You can even add multiple EH steps if your function is complex enough. Setting things up this way allows you to find the particular function where your logic error occurred.

3rd: In your last example, ending that function as a boolean is not the best method. If you were able to add the 2 arrays, then that function should return the resultant array. If not, it should throw up a msgbox-style error.

4th: I recently started doing a little trick that can be very helpful in some situations. In your VBA Editor, go to Tools->Options->General->Break on ALL errors. This is very helpful when you already have your error handling code in place, but you want to go the exact line where the error occurred and you don't feel like deleting perfectly good code.

Example: Let's say you want to catch an error that wouldn't be caught normally by VBA, i.e. an integer variable should always have a value >2. Somewhere in your code, say If intvar<=2 then goto EH. Then in your EH step, add If intvar<=2 then msgbox "Intvar=" & Intvar.