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.
First of all, let's clear up some misconceptions. There is no "bottom value". The bottom type is defined as a type that is a subtype of every other type in the language. From this, one can prove (in any interesting type system at least), that the bottom type has no values - it is empty. So there is no such thing as a bottom value.
Why is the bottom type useful? Well, knowing that it's empty let's us make some deductions on program behavior. For example, if we have the function:
def do_thing(a: int) -> Bottom: ...
we know that do_thing
can never return, since it would have to return a value of type Bottom
. Thus, there are only two possibilities:
do_thing
does not halt
do_thing
throws an exception (in languages with an exception mechanism)
Note that I created a type Bottom
which does not actually exist in the Python language. None
is a misnomer; it is actually the unit value, the only value of the unit type, which is called NoneType
in Python (do type(None)
to confirm for yourself).
Now, another misconception is that functional languages do not have exception. This isn't true either. SML for example has a very nice exception mechanism. However, exceptions are used much more sparingly in SML than in e.g. Python. As you've said, the common way to indicate some kind of failure in functional languages is by returning an Option
type. For example, we would create a safe division function as follows:
def safe_div(num: int, den: int) -> Option[int]:
return Some(num/den) if den != 0 else None
Unfortunately, since Python doesn't actually have sum types, this isn't a viable approach. You could return None
as a poor-man's option type to signify failure, but this is really no better than returning Null
. There is no type-safety.
So I would advise following the language's conventions in this case. Python uses exceptions idiomatically to handle control flow (which is bad design, IMO, but it's standard nonetheless), so unless you're only working with code you wrote yourself, I'd recommend following standard practice. Whether this is "pure" or not is irrelevant.
Best Answer
The main concern I have found with logging close to the source is that you are in danger of logging the same errors twice. In the second case we might imagine someone calling
DoSomethingElse()
without being sure whether it would log the error. These cases become increasingly common as the size and complexity of the component increases, and the number of times an error is logged scales with size.When errors are logged multiple times in multiple places, the density of useful information in the logs decreases and so the value of those logs also decreases.
When programmers instead follow the first example and log from a higher level there is much less danger of logs being repeated. Furthermore, at higher levels logs can include much more system meaning.
DoSomethingElse()
may only be trying to send a SQL string to a Postgres database, whilemain
is trying to purge all expired user accounts.The logs from
DoSomethingElse
can only include very low level information -- the SQL query that was sent, the connection string tried, etc. -- whilemain
can provide all of these (because the error was transmitted upwards) but also provide a high level description, e.g. "Could not connect to the database while attempting to purge stale accounts.".These high level descriptions make logs more valuable by providing useful glosses on the errors encountered. So in general I would recommend logging from the highest level possible.