Given something like this:
proc sub(): string {.raises: [OSError].} =
raise newException(OSError, "OS")
...
proc main() =
sub()
How do I know what exceptions sub
can raise? I probably need to inspect sub itself.
What if I have a long chain of functions? e.g. main -> a -> b -> c. Where c raises? I will probably find at runtime that I need to handle that exception somewhere.
As far as I can tell the compiler doesn't force me to handle the exception.
I can write something like:
proc main() =
let response = sub()
... do something with response
I can ignore that response may raise. Nothing enforces me to code for all possible outcomes.
It is not uncommon to see some libraries using exceptions for control flow e.g.
let response = request("http://foo/bar")
This request lib might raise an exception NotFound
when reaching a 404. This is not an exceptional circustance and exceptions should not be used for this. But lib author will do it as they can.
To handle an exception you need to wrap things like:
proc main() =
try:
sub()
except:
...
Compared to the alternative this code is noisier.
The alternative is to use Option/Maybe and Result/Either everywhere.
See Rust, Elm, Haskell for examples.
For example this raise index out of bounds
.
let se = @[1]
echo se[1]
Instead
let se = @[1]
Would give us an option[int]
. Option could be None
or Some[int]
. This encapsulate the possibility of failure.
Another example:
let response = doSomething()
response
could be a Result
. This result could be Err[string]
or Ok[T]
.
- Added safety:
The application doesn't crash unless a critical exceptional circustance e.g. a Panic in Rust.
Enforce handling all possible outcomes
-
By giving you back an Option or Result we must have a case expression to handle these, the compiler can then enforce that you handle all possible outcomes.
-
They are not surprising. If a proc signature says
proc sub(): Result[string, int] =
. You know exactly what you get.
These (Option and Result) looks like an extra layer of work at the beginning but we learn how to deal with these without making the code too noisy. For example is common to do pipelines with these, only retriving the result at the end:
let result = sub()
.andThen(doSomethingElse)
.andThen(doSomethingMore)
.withDefault("")
Here we never raise. If one of the proc returns a error result the whole chain is an error.
Rust has unwrap()
if you really want to panic, but this should be used when the program cannot recuperate from the error.
Another example
Imagine that b is something I import. By looking at the signature of
b
I cannot tell that it could raise an exception.