According to the wikipedia entry for side effect, raising an exception constitutes a side effect. Consider this simple python function:
def foo(arg):
if not arg:
raise ValueError('arg cannot be None')
else:
return 10
Invoking it with foo(None)
will always be met with an exception. Same input, same output. It is referentially transparent. Why is this not a pure function?
Purity is only violated if you observe the exception, and make a decision based on it that changes the control flow. Actually throwing an exception value is referentially transparent -- it is semantically equivalent to non-termination or other so-called bottom values.
If a (pure) function is not total, then it evaluates to a bottom value. How you encode the bottom value is up to the implementation - it could be an exception; or non-termination, or dividing by zero, or some other failure.
Consider the pure function:
f :: Int -> Int
f 0 = 1
f 1 = 2
This is not defined for all inputs. For some it evaluates to bottom. The implementation encodes this by throwing an exception. It should be semantically equivalent to using a Maybe
or Option
type.
Now, you only break referential transparency when you observe the bottom value, and make decisions based on it -- which could introduce non-determinism as many different exceptions may be thrown, and you can't know which one. So for this reason catching exceptions is in the IO
monad in Haskell, while generating so-called "imprecise" exceptions can be done purely.
So it is just not true that raising an exception is a side effect as such. It is whether or not you can modify the behavior of a pure function based on an exceptional value -- thus breaking referential transparency -- that is the issue.