7

The documentation says:

The throwIO variant should be used in preference to throw to raise an exception within the IO monad because it guarantees ordering with respect to other IO operations, whereas throw does not.

I'm still confused after reading it. Is there an example to show that throw will cause a problem whereas throwIO will not?

Additional question:

Is the following statement correct?

  1. If throw is being used to throw an exception in an IO, then the order of the exception is not guaranteed.
  2. If throw is being used to throw an exception in a non-IO value, then the order of the exception is guaranteed.

If I need to throw an exception in a Monad Transformer, which I have to use throw instead of throwIO, does it guarantee the order of the exception?

Leo Zhang
  • 3,040
  • 3
  • 24
  • 38

2 Answers2

5

I think the docs could be improved. The issue you need to keep in mind with throw and the like is that throw returns a bottom value that "explodes" (raises an exception) when evaluated; but whether and when evaluation happens is difficult to control due to laziness.

For example:

Prelude Control.Exception> let f n = if odd n then throw Underflow else True
Prelude Control.Exception> snd (f 1, putStrLn "this is fine")
this is fine

This could arguably be what you want to happen, but usually not. e.g. rather than the tuple above you might end up with a big data structure with a single exploding element that causes an exception to be raised after your web server returns 200 to the user, or something.

throwIO allows you to sequence raising an exception just as if it was another IO action, so it can be tightly controlled:

Prelude Control.Exception> throwIO Underflow >> putStrLn "this is fine"
*** Exception: arithmetic underflow

...just like doing print 1 >> print 2.

But note that you can actually replace throwIO with throw, for instance:

Prelude Control.Exception> throw Underflow >> putStrLn "this is fine"
*** Exception: arithmetic underflow

Since now the exploding value is of type IO a. It's actually not clear to me why throwIO exists other than to document an idiom. Maybe someone else can answer that.

As a final example, this has the same issue as my first example:

Prelude Control.Exception> return (throw Underflow) >> putStrLn "this is fine"
this is fine
jberryman
  • 16,334
  • 5
  • 42
  • 83
  • 2
    There's a bit more to it than this, too. `throw` results in imprecise exceptions - GHC is allowed to rewrite code in weird ways when they're involved, because GHC is allowed to consider all bottoms to be equivalent. `throwIO` prevents rewriting in that way - the exception you get will be the one thrown in the way you'd expect. – Carl Jan 02 '19 at 01:23
  • Thanks, here's a relevant answer on that point: https://stackoverflow.com/questions/11070690/how-do-exceptions-in-haskell-work – jberryman Jan 02 '19 at 03:22
  • What is a Haskell program? A Haskell program is a pure set of equations that produces an imperative program called `main`, and the data type for “imperative program” is `IO ()`. throw, error, and undefined all cause a *pure* expression to fail. They don’t just mean an error has ocurred in the program; they mean *there is no program*. throwIO is a well-formed program that fails. It actually confuses *me* that many functions in the standard library, e.g. `catch`, don’t make a distinction between them. – HTNW Jan 02 '19 at 13:37
2

throw is a generalization of undefined, while throwIO is an actual IO action. A key difference is that many laws don't quite hold when strictness is considered (i.e., when you have undefined (or throw) and seq).

> (throw Underflow :: IO ()) `seq` ()
*** Exception: arithmetic underflow
> (throw Underflow >>= pure) `seq` ()
()

Hence contradicting the law m >>= pure = m. throwIO doesn't have that issue, so it is the more principled way of throwing exceptions.

Li-yao Xia
  • 31,896
  • 2
  • 33
  • 56