Thursday, December 12, 2019

Prefer to use fail for IO exceptions

fail

This post briefly explains why I commonly suggest that people replace error with fail when raising IOExceptions.

The main difference between error and fail can be summarized by the following equations:

In other words, any attempt to evaluate an expression that is an error will raise the error. Evaluating an expression that is a fail does not raise the error or trigger any side effects.

Why does this matter? One of the nice properties of Haskell is that Haskell separates effect order from evaluation order. For example, evaluating a print statement is not the same thing as running it:

This insensitivity to evaluation order makes Haskell code easier to maintain. Specifically, this insensitivity frees us from concerning ourselves with evaluation order in the same way garbage collection frees us from concerning ourselves with memory management.

Once we begin using evaluation-sensitive primitives such as error we necessarily need to program with greater caution than before. Now any time we manipulate a subroutine of type IO a we need to take care not to prematurely force the thunk storing that subroutine.

How likely are we to prematurely evaluate a subroutine? Truthfully, not very likely, but fortunately taking the extra precaution to use fail is not only theoretically safer, it is also one character shorter than using error.

Limitations

Note that this advice applies solely to the case of raising IOExceptions within an IO subroutine. fail is not necessarily safer than error in other cases, because fail is a method of the MonadFail typeclass and the typeclass does not guarantee in general that fail is safe.

fail happens to do the correct thing for IO:

… but for other MonadFail instances fail could be a synonym for error and offer no additional protective value.

If you want to future-proof your code and ensure that you never use the wrong MonadFail instance, you can do one of two things:

  • Enable the TypeApplications language extension and write fail @IO string
  • Use Control.Exception.throwIO (userError string) instead of fail

However, even if you choose not to future-proof your code fail is still no worse than error in this regard.