Why is the raising of an exception a side effect?

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?


Solution 1:

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.

Solution 2:

From the first line:

"In computer science, a function or expression is said to have a side effect if, in addition to returning a value, it also modifies some state or has an observable interaction with calling functions or the outside world"

The state it modifies is the termination of the program. To answer your other question about why it is not a pure function. The function is not pure because throwing an exception terminates the program therefore it has a side effect (your program ends).

Solution 3:

Referential transparency is also the possibility to replace a computation (e.g. a function invocation) with the result of the computation itself, something that you can't do if your function raises an exception. That's because exceptions do not take part of computation but they need to be catch!

Solution 4:

I realize this is an old question, but the answers here are not wholly correct, IMHO.

Referential transparency refers to the property an expression has if the program it belongs to has the exact same meaning should the expression be replaced by its result. It should be clear that throwing an exception violates referential transparency, and consequently has side-effects. Let me demonstrate why...

I'm using Scala for this example. Consider the following function, which takes an integer argument, i, and adds an integer value, j, to it, then returns the result as an integer. If an exception occurs while adding the two values, then it returns the value 0. Alas, the calculation of j's value results in an exception being thrown (for simplicity, I've replaced j's initialization expression with the resulting exception).

def someCalculation(i: Int): Int = {
  val j: Int = throw new RuntimeException("Something went wrong...")
  try {
    i + j
  }
  catch {
    case e: Exception => 0 // Return 0 if we catch any exception.
  }
}

OK. It's a little dumb, but I'm trying to prove a point with a very simple case. ;-)

Let's define and call this function in the Scala REPL and see what we get:

$ scala
Welcome to Scala 2.13.0 (OpenJDK 64-Bit Server VM, Java 11.0.4).
Type in expressions for evaluation. Or try :help.

scala> :paste
// Entering paste mode (ctrl-D to finish)

def someCalculation(i: Int): Int = {
  val j: Int = throw new RuntimeException("Something went wrong...")
  try {
    i + j
  }
  catch {
    case e: Exception => 0 // Return 0 if we catch any exception.
  }
}

// Exiting paste mode, now interpreting.

someCalculation: (i: Int)Int

scala> someCalculation(8)
java.lang.RuntimeException: Something went wrong...
  at .someCalculation(<console>:2)
  ... 28 elided    

OK, so obviously, an exception occured. No surprises there.

But remember, an expression is referentially transparent if we can replace it by it's result such that the program has the exact same meaning. In this case, the expression we're focusing on is j. Let's refactor the function and replace j with its result (it's necessary to declare the type of the thrown exception to be an integer, because that is j's type):

def someCalculation(i: Int): Int = {
  try {
    i + ((throw new RuntimeException("Something went wrong...")): Int)
  }
  catch {
    case e: Exception => 0 // Return 0 if we catch any exception.
  }
}

Now let's re-evaluate that in the REPL:

scala> :paste
// Entering paste mode (ctrl-D to finish)

def someCalculation(i: Int): Int = {
  try {
    i + ((throw new RuntimeException("Something went wrong...")): Int)
  }
  catch {
    case e: Exception => 0 // Return 0 if we catch any exception.
  }
}

// Exiting paste mode, now interpreting.

someCalculation: (i: Int)Int

scala> someCalculation(8)
res1: Int = 0

Well, I guess you probably saw that coming: we had a different result that time around.

If we calculate j and then attempt to use it in a try block, then the program throws an exception. However, if we just replace j with its value in the block, we get a 0. So throwing exceptions has clearly violated referential transparency.

How should we proceed in a functional manner? By not throwing exceptions. In Scala (there are equivalents in other languages), one solution is to wrap possibly failing results in the Try[T] type: if successful, the result will be a Success[T] wrapping the successful result; if a failure occurs, then the result will be a Failure[Throwable] containing the associated exception; both expressions are sub-types of Try[T].

import scala.util.{Failure, Try}

def someCalculation(i: Int): Try[Int] = {
  val j: Try[Int] = Failure(new RuntimeException("Something went wrong..."))

  // Honoring the initial function, if adding i and j results in an exception, the
  // result is 0, wrapped in a Success. But if we get an error calculating j, then we
  // pass the failure back.
  j.map {validJ =>
    try {
      i + validJ
    }
    catch {
      case e: Exception => 0 // Result of exception when adding i and a valid j.
    }
  }
}

Note: We still use exceptions, we just don't throw them.

Let's try this in the REPL:

scala> :paste
// Entering paste mode (ctrl-D to finish)

import scala.util.{Failure, Try}

def someCalculation(i: Int): Try[Int] = {
  val j: Try[Int] = Failure(new RuntimeException("Something went wrong..."))

  // Honoring the initial function, if adding i and j results in an exception, the
  // result is 0, wrapped in a Success. But if we get an error calculating j, then we
  // pass the failure back.
  j.map {validJ =>
    try {
      i + validJ
    }
    catch {
      case e: Exception => 0 // Result of exception when adding i and a valid j.
    }
  }
}

// Exiting paste mode, now interpreting.

import scala.util.{Failure, Try}
someCalculation: (i: Int)scala.util.Try[Int]

scala> someCalculation(8)
res2: scala.util.Try[Int] = Failure(java.lang.RuntimeException: Something went wrong...)

This time, if we replace j with its value, we get the exact same result, and that's true in all cases.

However, there's another perspective on this: if the reason that an exception was thrown when calculating the value of j was due to some bad programming on our part (a logic error), then throwing the exception—which would result in terminating the program—may be regarded as an excellent way to bring the problem to our attention. However, if the exception is down to circumstances beyond our immediate control (such as the result of the integer addition overflowing), and we ought to be able to recover from such a condition, then we should formalize that possibility as part of the function's return value, and use, but not throw, an exception.