Why would a library (wreq
on a 404
for example) throw an exception instead of wrapping the result into something like Maybe
?
Naively, I think Maybe
would be better (compiler warning me if I'm not handling all the cases for example). Why am I wrong here?
There are many opinion based answers, but there is one answer which absolutely everyone should agree upon: If your library throws an exception, the user must catch it to handle it. Thus, the user's opinion about how you should throw exceptions is the right one.
There are three different kinds of exceptions in Haskell: one is imprecise; two are precise, of which one is synchronous, and one asynchronous. These were all identified in foundational Haskell papers 1.
The Haskell runtime system allows any IO action to throw runtime exceptions. Many common library functions throw runtime exceptions. There is no indication at the type level if something throws an exception. You should assume that, unless explicitly documented otherwise, all actions may throw an exception.
Haskellers do strongly strive to avoid throwing exceptions from functions – a function should only throw an exception in truely exceptional cases, i.e. in “this should never happen” situations like the user passing some input that's explicitly forbidden by the documentation. If pure functions regularly threw exceptions, this would be a big problem not only because the type doesn't say what one should be prepared to catch, also one can't catch exceptions within a pure function but only in IO
code that calls the function. And even if you can in principle catch the exception, it may be hard to predict where it needs to be done because lazy evaluation can delay the point where it actually happens.
As a matter of fact, even in case of e.g. Wreq.get
, no exception is thrown from the function.
Prelude Network.Wreq> get "htt:/p/this-isn't even valid URL syntax" `seq` "Ok"
"Ok"
It is the IO
action that throws, when you execute it:
Prelude Network.Wreq> get "htt:/p/this-isn't even valid URL syntax" >> pure ()
*** Exception: InvalidUrlException "htt:/p/this-isn't%20even%20valid%20URL%20syntax" "Invalid scheme"
Now with an IO
action, the situation is a bit different. Lots of IO
actions can have potentially very different errors in different situations that may be hard or impossible to predict, like a hard-drive crash. Catalogising all the possible errors in a suitable data type for each action would be a major undertaking, and it would be really quite cumbersome to handle every possible case or figure out which parts just to pass on. And simply wrapping the result of every single IO
action in Maybe
would just lead to a similar situation as in Java where every reference can possibly null. This doesn't tell you anything, and people often wouldn't come up with sensible ways of handling this either.
This is pretty much the problem why exceptions were invented in the first place, and it holds just as well for procedural languages as it holds for Haskell (or rather, it's procedural eDSL that is IO
). And because unlike pure functions, IO
does have a well-defined time-sequence in its control flow, it's also pretty clear where you must do it if you need to catch some particular exception.
That's not to say it never makes sense for an IO
action to return a Maybe
/ Either
value that makes the possible errors explicit, just this isn't always worthwhile.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With