Skip to content

Instantly share code, notes, and snippets.

@sporto
Last active October 4, 2017 02:08
Show Gist options
  • Save sporto/ea82c956d9d0d5ff633cd6825363a3e9 to your computer and use it in GitHub Desktop.
Save sporto/ea82c956d9d0d5ff633cd6825363a3e9 to your computer and use it in GitHub Desktop.
What is wrong with exceptions?

Exceptions are surprising

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.

Exceptions let me ignore possible errors

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.

People use exceptions for control flow

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.

Exception handling is verbose:

To handle an exception you need to wrap things like:

proc main() =
    try:
       sub()
    except:
       ...

Compared to the alternative this code is noisier.

What is an alternative?

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].

Benefits:

  • 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.

Handling

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.

@sporto
Copy link
Author

sporto commented Oct 4, 2017

Another example

proc c(): int {.raises: [OSError].} =
    raise newException(OSError, "OS")

proc b(): int =
    c()

proc main() =
    let int = b()

Imagine that b is something I import. By looking at the signature of b I cannot tell that it could raise an exception.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment