Should exceptions be case classes?

This is very subjective of course, but in my opinion it is good practice to have exception classes as case classes. The main rationale being that when you catch an exception, you are doing pattern matching, and case classes are much nicer to use in pattern matching. Here's an example that takes advantage of the ability to use the full power of pattern matching in a catch block, when using a case class exception:

object IOErrorType extends Enumeration {
  val FileNotFound, DeviceError, LockedFile = Value
}
case class IOError(message: String, errorType: IOErrorType.Value) extends  Exception(message)

def doSomeIO() { throw IOError("Oops, file not found!", IOErrorType.FileNotFound)  }

try {
  doSomeIO()
} catch {
  case IOError( msg, IOErrorType.FileNotFound ) =>
    println("File not found, please check the path! (" + msg + ")")
}

In this example, we have only one exception, but it contains an errorType field for when you want to know the exact error type that occurred (usually this is modelled through a hierarchy of exception, I'm not saying this is better or worse, the example is just illustrative). Because the IOError is a case class I can simply do case IOError( msg, IOErrorType.FileNotFound ) to catch the exception only for the error type IOErrorType.FileNotFound. Without the extractors that we get for free with case classes, I would have to catch the exception everytime, and then rethrow in case I'm actually not interested, which is definitely more verbose.

You say that case classes give you incorrect equality semantics. I don't think so. You, as the writer of the exception class gets to decides what equality semantics makes sense. After all when you catch an exception, the catch block is where you decide which exceptions to catch usually based on the type alone, but could be based on the value of its fields or whatever, as in my example. The point is that the equality semantics of the exception class has little to do with that.


One common idiom you lose by making exceptions case classes is the pattern of creating a subclass hierarchy of exceptions with subclassing used to indicate greater specificity of the error condition. Case classes can't be subclassed.


I like the answer from Régis Jean-Gilles. But if you have a good reason to not make a case class (see answer of Dave Griffith), you can archieve the same as in the sample above with a normal class and unapply:

object IOErrorType extends Enumeration {
  val FileNotFound, DeviceError, LockedFile = Value
}
object IOError {
  def unapply(err: IOError): Option[(String, IOErrorType.Value)] = Some(err.message, err.errorType)
}
class IOError(val message: String, val errorType: IOErrorType.Value) extends  Exception(message)

def doSomeIO() { throw new IOError("Oops, file not found!", IOErrorType.FileNotFound) }

try {
  doSomeIO()
} catch {
  case IOError( msg, IOErrorType.FileNotFound ) =>
    println("File not found, please check the path! (" + msg + ")")
}