63

I'm in the middle of developing a new programming language to solve some business requirements, and this language is targeted at novice users. So there is no support for exception handling in the language, and I wouldn't expect them to use it even if I added it.

I've reached the point where I have to implement the divide operator, and I'm wondering how to best handle a divide by zero error?

I seem to have only three possible ways to handle this case.

  1. Ignore the error and produce 0 as the result. Logging a warning if possible.
  2. Add NaN as a possible value for numbers, but that raises questions about how to handle NaN values in other areas of the language.
  3. Terminate the execution of the program and report to the user a severe error occurred.

Option #1 seems the only reasonable solution. Option #3 is not practical as this language will be used to run logic as a nightly cron.

What are my alternatives to handling a divide by zero error, and what are the risks with going with option #1.

34
  • 13
    if you did add exception support and the user didn't catch it then you'd have option #3 Aug 18, 2013 at 11:54
  • 85
    I'm curious, what kind of stupid requirement would require you to create a whole new programming language? In my experience, every language ever created sucks (in design or in execution, often in both) and it took unreasonably much effort to even get that much. There are a few exceptions to the first, but not to the second, and as they're easily <0.01% of the cases, they're probably measurement errors ;-)
    – user7043
    Aug 18, 2013 at 12:04
  • 16
    @delnan most new languages are created to allow business rules to be separated from how they are implemented. The user does not need to know how reject "Foo" was implemented, but simply that it rejects a document if it contains the keyword Foo. I try to make the language as easy to read using terms the user is familiar with. Giving a user their own programming language empowers them to add business rules without depending upon technical staff.
    – Reactgular
    Aug 18, 2013 at 12:15
  • 20
    @Mathew Foscarini. Never, ever, ignore the error and silently return 0. When doing a division, 0 may be a perfectly legal value (for some reason, there is such a thing in Power Basic, and it's really a pain). If you divie floating point numbers, Nan or Inf would be nice (have a look at IEEE 754 to understand why). If you divide integers, you may stop the program, divide by 0 should never be allowed (well, unless you want to implement a true exception system).
    – user66888
    Aug 19, 2013 at 6:07
  • 18
    I amused and fascinated by a business domain complex enough to justify a proprietary, Turing-complete programming language but lax enough to tolerate drastically inaccurate results. Jul 12, 2014 at 20:06

16 Answers 16

101

I would strongly advise against #1, because just ignoring errors is a dangerous anti-pattern. It can lead to hard-to-analyze bugs. Setting the result of a division by zero to 0 makes no sense whatsoever, and continuing program execution with a nonsensical value is going to cause trouble. Especially when the program is running unattended. When the program interpreter notices that there is an error in the program (and a division-by-zero is almost always a design error), aborting it and keeping everything as-is is usually preferred over filling your database with garbage.

Also, you will unlikely be successful with thoroughly following this pattern through. Sooner or later you will run into error situations which just can't be ignored (like running out of memory or a stack overflow) and you will have to implement a way to terminate the program anyway.

Option #2 (using NaN) would be a bit of work, but not as much as you might think. How to handle NaN in different calculations is well-documented in the IEEE 754 standard, so you can likely just do what the language your interpreter is written in does.

By the way: Creating a programming language usable by non-programmers is something we've been trying to do since 1964 (Dartmouth BASIC). So far, we've been unsuccessful. But good luck anyway.

11
  • 14
    +1 thanks. You convinced me to throw an error, and now that I read your answer I don't understand why I was hesitating. PHP has been a bad influence on me.
    – Reactgular
    Aug 18, 2013 at 14:17
  • 27
    Yes, it has. When I read your question, I immediately thought that it was a very PHP-esque thing to produce incorrect output and keep trucking along in the face of errors. There are good reasons why PHP is the exception in doing this.
    – Joel
    Aug 18, 2013 at 16:14
  • 4
    +1 for the BASIC comment. I don't advise using NaN in a beginner's language, but in general, great answer. Aug 19, 2013 at 11:15
  • 8
    @Joel If he'd lived long enough, Dijkstra would probably have said "The use of [PHP] cripples the mind; its teaching should, therefore, be regarded as a criminal offense." Aug 19, 2013 at 11:16
  • 13
    @Ross. "arrogance in computer science is measured in nano-Dijkstras" -- Alan Kay
    – user66888
    Aug 19, 2013 at 12:44
33

1 - Ignore the error and produce 0 as the result. Logging a warning if possible.

That's not a good idea. At all. People will start depending on it and should you ever fix it, you will break a lot of code.

2 - Add NaN as a possible value for numbers, but that raises questions about how to handle NaN values in other areas of the language.

You should handle NaN the way runtimes of other languages do it: Any further calculation also yields NaN and every comparison (even NaN == NaN) yields false.

I think this is acceptable, but not necessarily new comer friendly.

3 - Terminate the execution of the program and report to the user a severe error occurred.

This is the best solution I think. With that information in hand users should be able to handle 0. You should provide a testing environment, especially if it's intended to run once a night.

There's also a fourth option. Make division a ternary operation. Any of these two will work:

  • div(numerator, denumerator, alternative_result)
  • div(numerator, denumerator, alternative_denumerator)
6
  • But if you make NaN == NaN be false, then you will have to add a isNaN() function so that users are able to detect NaNs. Aug 18, 2013 at 20:54
  • 2
    @AJMansfield: Either that, or people implement it themselves: isNan(x) => x != x. Still, when you have NaN coming up in your programming code, you should not start adding isNaN checks, but rather track down the cause and make the necessary checks there. Therefore it is important for NaN to propagate fully.
    – back2dos
    Aug 18, 2013 at 21:31
  • 5
    NaNs are majorly counter-intuitive. In a beginner's language, they're dead on arrival. Aug 19, 2013 at 11:17
  • 2
    @RossPatterson But a beginner can easily say 1/0 -- you have to do something with it. There is no possibly useful result other than Inf or NaN -- something that will propagate the error further into the program. Otherwise the only solution is to stop with an error at this point.
    – Mark Hurd
    Aug 21, 2013 at 1:23
  • 1
    Option 4 could be improved by allowing the invocation of a function, which in turn could perform whatever action is necessary to recover from the unexpected 0 divisor.
    – CyberFonic
    Jul 14, 2014 at 6:41
22

Terminate the running application with extreme prejudice. (While providing adequate debug information)

Then educate your users to identify and handle conditions where the divisor might be zero (user entered values, etc.)

13

In Haskell (and similar in Scala), instead of throwing exceptions (or returning null references) the wrapper types Maybe and Either can be used. With Maybe the user has a chance to test if the value he got is "empty", or he might provide a default value when "unwrapping". Either is similar, but can be used returns an object (e.g. an error string) describing the problem if there is one.

4
  • 1
    True, but note that Haskell doesn't use this for division by zero. Instead, every Haskell type implicitly has "bottom" as a possible value. This isn't like null pointers in the sense that it's the "value" of an expression that fails to terminate. You can't test for nontermination as a value, of course, but in operational semantics the cases that fail to terminate are part of the meaning of an expression. In Haskell, that "bottom" value also handles additional error-case results such as the error "some message" function being evaluated.
    – user8709
    Nov 15, 2013 at 16:46
  • Personally, if the effect of aborting the whole program is considered valid, I don't know why pure code can't have the effect of throwing an exception, but that's just me - Haskell doesn't allow pure expressions to throw exceptions.
    – user8709
    Nov 15, 2013 at 16:49
  • I think this is a good idea because other than throwing an exception, all the proposed options do not communicate to the users that they made a mistake. The basic idea is that the user makes a mistake with the value they gave to the program, so the program should tell the user that they gave wrong input (then the user can think of a way to remedy). Without telling the users about their mistake, any solution feels so odd.
    – InformedA
    Jul 13, 2014 at 5:03
  • I think this is the way to go... The Rust programming language uses it extensively in its standard library.
    – aochagavia
    Jul 13, 2014 at 15:13
12

Number 1 (insert undebuggable zero) is always bad. The choice between #2 (propagate NaN) and #3 (kill the process) depends on context and ideally should be a global setting, as it is in Numpy.

If you're doing one big, integrated calculation, propagating NaN is a bad idea because it will eventually spread and infect your whole calculation--- when you look at the results in the morning and see that they're all NaN, you'd have to throw out the results and start again anyway. It would have been better if the program terminated, you got a call in the middle of the night and fixed it--- in terms of the number of wasted hours, at least.

If you're doing many little, mostly-independent calculations (like map-reduce or embarrassingly parallel calculations), and you can tolerate some percentage of them being unusable due to NaNs, the that's probably the best option. Terminating the program and not doing the 99% that would be good and useful on account of the 1% that are malformed and divide by zero might be a mistake.

Another option, related to NaNs: the same IEEE floating-point specification defines Inf and -Inf, and these are propagated differently than NaN. For instance, I'm pretty sure that Inf > any number and -Inf < any number, which would be what you wanted if your division by zero happened because the zero was just supposed to be a small number. If your inputs are rounded and suffer from measument error (like physical measurements taken by hand), the difference of two large quantities can result in zero. Without the division-by-zero, you would have gotten some large number, and maybe you don't care how large it is. In that case, In and -Inf are perrfectly valid results.

It can be formally correct, too--- just say you're working in the extended reals.

3
  • But we can't tell whether the denominator was intended to be positive or negative, so division might yield +inf when -inf was desired, or visa versa. Nov 15, 2013 at 17:31
  • True, your measurement error is too small to distinguish between +inf and -inf. This most closely resembles the Riemann sphere, in which the whole complex plane is mapped to a ball with exactly one infinite point (the point diametrically opposite to the origin). Very large positive numbers, very large negative numbers, and even very large imaginary and complex numbers are all close to that one infinite point. With a little measurement error, you can't distinguish them. Nov 15, 2013 at 21:21
  • If you're working in that sort of system, you'd have to identify +inf and -inf as being equivalent, just as you have to identify +0 and -0 as being equivalent, even though they have different binary representations. Nov 15, 2013 at 21:22
12

Other answers have already considered the relative merits of your ideas. I propose another one: use basic flow analysis to determine whether a variable can be zero. Then you can simply disallow division by variables that are potentially zero.

x = ...
y = ...

if y ≠ 0:
  return x / y    // In this block, y is known to be nonzero.
else:
  return x / y    // This, however, is a compile-time error.

Alternatively, have an intelligent assert function that establishes invariants:

x = ...
require x ≠ 0, "Unexpected zero in calculation"
// For the remainder of this scope, x is known to be nonzero.

This is as good as throwing a runtime error—you skirt undefined operations entirely—but has the advantage that the code path need not even be hit for the potential failure to be exposed. It can be done much like ordinary typechecking, by evaluating all branches of a program with nested typing environments for tracking and verifying invariants:

x = ...           // env1 = { x :: int }
y = ...           // env2 = env1 + { y :: int }
if y ≠ 0:         // env3 = env2 + { y ≠ 0 }
  return x / y    // (/) :: (int, int ≠ 0) → int
else:             // env4 = env2 + { y = 0 }
  ...
...               // env5 = env2

Furthermore, it extends naturally to range and null checking, if your language has such features.

3
  • 4
    Neat idea, but this type of constraint solving is NP-complete. Imagine something like def foo(a,b): return a / ord(sha1(b)[0]). The static analyzer can't invert SHA-1. Clang has this type of static analysis and its great for finding shallow bugs but there are plenty of cases it can't handle. Jul 12, 2014 at 20:12
  • 9
    this is not NP-complete, this is impossible -- says halting lemma. However, the static analyzer doesn't need to solve this, it can just crap out on a statement like this and require you to add an explicit assertion or decoration.
    – MK01
    Jul 13, 2014 at 4:07
  • 1
    @MK01: In other words, the analysis is “conservative”.
    – Jon Purdy
    Jul 13, 2014 at 19:04
8

3. Terminate the execution of the program and report to the user a severe error occurred.

[This option] is not practical…

Of course it is practical: It is the programmers' responsibility to write a program that actually makes sense. Dividing by 0 does not make any sense. Therefore, if the programmer is performing a division, it is also his/her responsibility to verify beforehand that the divisor is not equal to 0. If the programmer fails to perform that validation check, then s/he should realize that mistake as soon as possible, and denormalized (NaN) or incorrect (0) computation results simply won't help in that respect.

Option 3 happens to be the one I would have recommended to you, btw, for being the most straightforward, honest, and mathematically correct one.

4

It seems like a bad idea to me to run important tasks (ie; "nightly cron") in an environment where errors are ignored. It's a terrible idea to make this a feature. This rules out options 1 and 2.

Option 3 is the only acceptable solution. Exceptions don't have to be part of the language, but they are part of reality. Your termination message ought to be as specific and informative as possible about the error.

4

IEEE 754 actually has a well defined solution for your problem. Exception handling without using exceptions http://en.wikipedia.org/wiki/IEEE_floating_point#Exception_handling

1/0  = Inf
-1/0 = -Inf
0/0  = NaN

this way all your operations make mathematically sense.

\lim_{x \to 0} 1/x = Inf

In my opinion following IEEE 754 makes the most sense since it ensures that your calculations are as correct as it going to be on a computer and you are also consistent with the behaviour of other programming languages.

The only problem that arises is that Inf and NaN are gonna contaminate your results and your users will not know exactly where the problem is coming from. Take a look at a language like Julia that does this quite well.

julia> 1/0
Inf

julia> -1/0
-Inf

julia> 0/0
NaN

julia> a = [1,1,1] ./ [2,1,0]
3-element Array{Float64,1}:
   0.5
   1.0
 Inf

julia> sum(a)
Inf

julia> a = [1,1,0] ./ [2,1,0]
3-element Array{Float64,1}:
   0.5
   1.0
 NaN

julia> sum(a)
NaN

The division error is correctly propagated through the mathematical operations but at the end the user does not necessarily know from which operation the error stems from.

edit: I didn't see the second part of the answer by Jim Pivarski which is basically what I am saying above. My bad.

2

SQL, easily the language most widely used by non-programmers, does #3, for whatever that's worth. In my experience observing and assisting non-programmers writing SQL, this behavior is generally well understood and easily compensated for (with a case statement or the like). It helps that the error message you get tends to be quite direct e.g. in Postgres 9 you get "ERROR: division by zero".

2

I think the problem is " targeted at novice users. --> So there is no support for ..."

Why are you thinking that exception handling is problematic for novice users?

What is worse? Have a "difficult" feature or have no idea why something happened? What could confuse more? A crash with a core dump or "Fatal error: Divide by Zero"?

Instead, I think is FAR better to aim for GREAT message errors. So do instead: "Bad calculation, Divide 0 / 0" (ie: Always show the DATA that cause the problem, not just the kind of problem). Look at how PostgreSql do the message errors, that are great IMHO.

However, you can look at other ways to work with exceptions like:

http://dlang.org/exception-safe.html

I also have dream on build a language, and in this case I think that mix a Maybe/Optional with normal Exceptions could do be the best:

def openFile(fileName): File | Exception
    if not(File.Exist(fileName)):
        raise FileNotExist(fileName)
    else:
        return File.Open()

#This cause a exception:

theFile = openFile('not exist')

# But this, not:

theFile | err = openFile('not exist')
2

There are two fundamental reasons for a divide by zero.

  1. In a precise model (like integers), you get a divide by zero DBZ because the input is wrong. This is the kind of DBZ that most of us think of.
  2. In non-precise model (like floating pt), you might get a DBZ because of rounding-error even though the input is valid. This is what we don't normally think of.

For 1. you have to communicate back to the users that they made a mistake because they are the one responsible and they are the one who know best how to remedy the situation.

For 2. This is not user fault, you can finger point to algorithm, hardware implementation, etc but this is not user's fault so you shouldn't terminate the program or even throw exception (if allowed which is not in this case). So a reasonable solution is to continue the operations in some reasonable manner.

I can see the the person asking this question asked for case 1. So you need to communicate back to the user. Using whatever floating point standard, Inf, -Inf, Nan, IEEE doesn't fit in this situation. Fundamentally wrong strategy.

1

In my view your language should provide a generic mechanism for detecting and handling errors. Programming errors should be detected at compile time (or as early as possible) and should ordinarily lead to program termination. Errors that result from unexpected or erroneous data, or from unexpected external conditions, should be detected and made available for appropriate action, but allow the program to continue whenever possible.

Plausible actions include (a) terminate (b) prompt the user for an action (c) log the error (d) substitute a corrected value (e) set an indicator to be tested in code (f) invoke an error handling routine. Which of these you make available and by what means are choices you have to make.

From my experience, common data errors like faulty conversions, divide by zero, overflow and value out of range are benign and should by default be handled by substituting a different value and setting an error flag. The (non-programmer) using this language will see the faulty data and quickly understand the need to check for errors and handle them.

[For an example, consider an Excel spreadsheet. Excel does not terminate your spreadsheet because a number overflowed or whatever. The cell gets a strange value and you go find out why and fix it.]

So to answer your question: you should certainly not terminate. You might substitute NaN but you should not make that visible, just make sure the calculation completes and generates a strange high value. And set an error flag so that users who need it can determine that an error occurred.

Disclosure: I created just such a language implementation (Powerflex) and addressed exactly this problem (and many others) in the 1980s. There has been little or no progress on languages for non-programmers in the last 20 years or so, and you will attract heaps of criticism for trying, but I really hope you succeed.

1

I liked the ternary operator where you provide an alternate value in case the denumerator is 0.

One more idea I didn't see is to produce a general "invalid" value. A general "this variable doesn't have a value because the program did something bad", which carries a full stack trace with itself. Then, if you ever use that value anywhere, the result is again invalid, with the new operation attempted on top (i.e. if the invalid value ever appears in an expression, the entire expression yields invalid and no function calls are attempted; an exception would be boolean operators - true or invalid is true and false and invalid is false - there may be other exceptions to that, too). Once that value is no longer referenced anywhere, you log a nice long description of the entire chain where things were wrong and continue business as usual. Maybe email the trace to the project lead or something.

Something like the Maybe monad basically. It will work with anything else that can fail, too, and you can allow people to construct their own invalids. And the program will continue to run as long as the error isn't too deep, which is what is really wanted here, I think.

0

Disallow it in the language. That is to say, disallow dividing by a number until it's provably not zero, usually by testing it first. Ie.

int div = random(0,100);
int b = 10000 / div; // Error E0000: div might be zero
5
  • To do this you need a new numeric type, a natural number, as opposed to an integer. That could be...difficult...to deal with.
    – Servy
    Nov 15, 2013 at 18:24
  • @Servy: No, you wouldn't. Why would you? You do need logic in the compiler to determine possible values, but you want that anyway (for optimizing reasons).
    – MSalters
    Nov 15, 2013 at 18:27
  • If you don't have a different type, one for zero and one for non-zero values, then you wouldn't be able to solve the problem in the general case. You'd either have false positives, and force the user to check against zero way more often than they actually should, or you'll create situations where they can still divide by zero.
    – Servy
    Nov 15, 2013 at 18:29
  • @Servy: You're mistaken: a compiler can track that state without needing such a type, and for instance GCC already does so. E.g. the C type int allows zero values, but GCC can still determine where in the code specific ints can't be zero.
    – MSalters
    Nov 15, 2013 at 18:32
  • 2
    But only in certain cases; it can't do so, with 100% accuracy, in all cases. You'll either have false positives or false negatives. This is provably true. For example, I could create a snippet of code that may or may not even complete. If the compiler can't even know if it finished, how could it know if the resulting int is non-zero? It can catch simple obvious cases, but not all cases.
    – Servy
    Nov 15, 2013 at 18:35
0

As you a writing a programming language, you should take advantage of the fact and make it mandatory to include an action for the devise by zero state. a <= n / c :0 div-by-zero-action

I know what I've just suggested is essentially adding a 'goto' to your PL.

Not the answer you're looking for? Browse other questions tagged or ask your own question.