1. Overview

In this tutorial, we’re going to cover custom exceptions and multiple ways to handle exceptions in Scala — including a couple of ways to avoid exceptions altogether.

2. Custom Exceptions

Exactly like Java, we create custom exceptions in Scala by extending the Exception class.

Let’s create an exception:

case class DivideByZero() extends Exception

Here we create a custom exception DivideByZero. Once created, we can throw or catch custom exceptions as we do with other exceptions.

Let’s use our DivideByZero exception when we notice that someone is trying to divide by zero:

def divide(dividend: Int, divisor: Int): Int = {
  if (divisor == 0) {
    throw new DivideByZero
  }

  dividend / divisor
}

Next, let’s take a look at how we can handle exceptions in Scala. Unlike Java, Scala offers multiple ways to do it, and we can choose a method that best fits our needs.

3. try/catch/finally

The try/catch/finally keyword group is the most familiar way to handle exceptions.

Simply put, we wrap the risky code in a try block and the error handling in a catch block. Let’s handle our DivideByZero exception thrown by the divide method we created earlier:

def divideByZero(a: Int): Any = {
  try {
    divide(a, 0)
  } catch {
    case _: DivideByZero => null
  }
}

Here, we handle the DivideByZero exception by catching it and returning null.

Optionally, we can also add a finally block. Code in a finally block is always run, regardless of whether the risky code failed or not.

4. Try/Success/Failure

A cleaner way to handle exceptions is to use Try/Success/Failure. If the code succeeds, we return a Success object with the result, and if it fails, we pass the error in a Failure object.

Let’s implement divideByZero with Success/Failure:

def divideWithTry(dividend: Int, divisor: Int): Try[Int] = Try(divide(dividend, divisor))

When we call divideWithTry, we get a Failure object that contains the original error:

assert(divideWithTry(10, 0) == Failure(new DivideByZero))

Callers of divideWithTry can pattern match using Success and Failure objects*,* like so:

val result = divideWithTry(10, 0) match {
  case Success(i) => i
  case Failure(DivideByZero()) => None
}

5. Exception-less Error Handling

So far, we’ve looked at how we can handle exceptions, but let’s see what else we can do.

Our divide-by-zero scenario can be solved without exceptions, and such is often the case when coding. There are performance advantages to returning an object instead of making the JVM generate a stack trace.

So, following the adage, “never throw an exception you intend to catch”, let’s also take a look at a couple of exception-less ways to address our use case.

5.1. Option/Some/None

One drawback of the divideByZero method’s try/catch/finally handling is that it returns null. This makes it cumbersome for its clients to handle the result.

Instead of returning null, we can return an Option:

def divideWithOption(dividend: Int, divisor: Int): Option[Int] = {
  if (divisor == 0) {
    None
  } else {
    Some(dividend / divisor)
  }
}

A couple of things change when we use Option. First, notice the return type of our function is now Option[Int] instead of Int. Under normal circumstances, that’ll mean we return a Some. Second, instead of an exception, we’ll return None.

Code that calls divideWithOption can pattern match using Some and None, just like we did for Success and Failure earlier*.*

Option has one drawback: it swallows the original error. There’s no way to let the caller know what exactly happened.

In cases when we want to return the error to the caller, we should still use Try/Success/Failure or we should take a look at our final approach.

5.2. Either/Left/Right

Another exception-less alternative is Either.

Either represents two, mutually exclusive, possible values represented by Left and Right.

When handling errors, the convention is to use Left for the error and Right for the result. Comparing it with Option/Some/None, Right is similar to Some and Left is similar to None.

Let’s rewrite our divide-by-zero method using Either:

def divideWithEither(dividend: Int, divisor: Int): Either[String, Int] = {
  if (divisor == 0) {
    Left("Can't divide by zero")
  } else {
    Right(dividend / divisor)
  }
}

Here, we return the value as Right and, in case of an error, we return Left with the original error. The caller can pattern match against Right and Left.

6. Conclusion

In this article, we went through how to create custom exceptions in Scala.

Then, we covered four ways to handle errors. We started with the traditional try/catch/finally and then followed up with Scala’s object-oriented approach. We finished up by showing how to address errors without throwing exceptions.

As always, all code examples can be found over on GitHub.


« 上一篇: Cake 模式