Kaarel Nummert
Kaarel Nummert

Reputation: 999

Scala try-catch not catching exception

As far as I understand, the following code snippet should execute fine. Stream("A", "").toIterable.map(_.head) throws an exception, which should be caught by the try block.

import org.scalatest.FunSuite

class TryCatchTest extends FunSuite {
  test("Try failure") {
    List(1).flatMap {
      _ =>
        try {
          Stream("A", "").toIterable.map(_.head)
        } catch {
          case e: Throwable => Nil
        }
    }
  }
}

But when I run it, it crashes with the following error:

next on empty iterator
java.util.NoSuchElementException: next on empty iterator
    at scala.collection.Iterator$$anon$2.next(Iterator.scala:39)
    at scala.collection.Iterator$$anon$2.next(Iterator.scala:37)
    at scala.collection.IndexedSeqLike$Elements.next(IndexedSeqLike.scala:63)
    at scala.collection.IterableLike$class.head(IterableLike.scala:107)
    at scala.collection.immutable.StringOps.scala$collection$IndexedSeqOptimized$$super$head(StringOps.scala:30)
    at scala.collection.IndexedSeqOptimized$class.head(IndexedSeqOptimized.scala:126)
    at scala.collection.immutable.StringOps.head(StringOps.scala:30)
    at TryCatchTest$$anonfun$1$$anonfun$apply$mcV$sp$1$$anonfun$apply$1.apply(TryCatchTest.scala:8)
    at TryCatchTest$$anonfun$1$$anonfun$apply$mcV$sp$1$$anonfun$apply$1.apply(TryCatchTest.scala:8)
    at scala.collection.immutable.Stream$$anonfun$map$1.apply(Stream.scala:418)
    at scala.collection.immutable.Stream$$anonfun$map$1.apply(Stream.scala:418)
    at scala.collection.immutable.Stream$Cons.tail(Stream.scala:1222)
    at scala.collection.immutable.Stream$Cons.tail(Stream.scala:1212)
    at scala.collection.immutable.Stream.foreach(Stream.scala:595)
    at scala.collection.immutable.List.flatMap(List.scala:327)
    at TryCatchTest$$anonfun$1.apply$mcV$sp(TryCatchTest.scala:5)
    at TryCatchTest$$anonfun$1.apply(TryCatchTest.scala:5)
    at TryCatchTest$$anonfun$1.apply(TryCatchTest.scala:5)
    at org.scalatest.Transformer$$anonfun$apply$1.apply$mcV$sp(Transformer.scala:22)
    at org.scalatest.OutcomeOf$class.outcomeOf(OutcomeOf.scala:85)
    at org.scalatest.OutcomeOf$.outcomeOf(OutcomeOf.scala:104)
    at org.scalatest.Transformer.apply(Transformer.scala:22)
    at org.scalatest.Transformer.apply(Transformer.scala:20)
    at org.scalatest.FunSuiteLike$$anon$1.apply(FunSuiteLike.scala:166)
    at org.scalatest.Suite$class.withFixture(Suite.scala:1122)
    at org.scalatest.FunSuite.withFixture(FunSuite.scala:1555)
    at org.scalatest.FunSuiteLike$class.invokeWithFixture$1(FunSuiteLike.scala:163)
    at org.scalatest.FunSuiteLike$$anonfun$runTest$1.apply(FunSuiteLike.scala:175)
    at org.scalatest.FunSuiteLike$$anonfun$runTest$1.apply(FunSuiteLike.scala:175)
    at org.scalatest.SuperEngine.runTestImpl(Engine.scala:306)
    at org.scalatest.FunSuiteLike$class.runTest(FunSuiteLike.scala:175)
    at org.scalatest.FunSuite.runTest(FunSuite.scala:1555)
    at org.scalatest.FunSuiteLike$$anonfun$runTests$1.apply(FunSuiteLike.scala:208)
    at org.scalatest.FunSuiteLike$$anonfun$runTests$1.apply(FunSuiteLike.scala:208)
    at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:413)
    at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:401)
    at scala.collection.immutable.List.foreach(List.scala:381)
    at org.scalatest.SuperEngine.traverseSubNodes$1(Engine.scala:401)
    at org.scalatest.SuperEngine.org$scalatest$SuperEngine$$runTestsInBranch(Engine.scala:396)
    at org.scalatest.SuperEngine.runTestsImpl(Engine.scala:483)
    at org.scalatest.FunSuiteLike$class.runTests(FunSuiteLike.scala:208)
    at org.scalatest.FunSuite.runTests(FunSuite.scala:1555)
    at org.scalatest.Suite$class.run(Suite.scala:1424)
    at org.scalatest.FunSuite.org$scalatest$FunSuiteLike$$super$run(FunSuite.scala:1555)
    at org.scalatest.FunSuiteLike$$anonfun$run$1.apply(FunSuiteLike.scala:212)
    at org.scalatest.FunSuiteLike$$anonfun$run$1.apply(FunSuiteLike.scala:212)
    at org.scalatest.SuperEngine.runImpl(Engine.scala:545)
    at org.scalatest.FunSuiteLike$class.run(FunSuiteLike.scala:212)
    at org.scalatest.FunSuite.run(FunSuite.scala:1555)
    at org.scalatest.tools.SuiteRunner.run(SuiteRunner.scala:55)
    at org.scalatest.tools.Runner$$anonfun$doRunRunRunDaDoRunRun$3.apply(Runner.scala:2563)
    at org.scalatest.tools.Runner$$anonfun$doRunRunRunDaDoRunRun$3.apply(Runner.scala:2557)
    at scala.collection.immutable.List.foreach(List.scala:381)
    at org.scalatest.tools.Runner$.doRunRunRunDaDoRunRun(Runner.scala:2557)
    at org.scalatest.tools.Runner$$anonfun$runOptionallyWithPassFailReporter$2.apply(Runner.scala:1044)
    at org.scalatest.tools.Runner$$anonfun$runOptionallyWithPassFailReporter$2.apply(Runner.scala:1043)
    at org.scalatest.tools.Runner$.withClassLoaderAndDispatchReporter(Runner.scala:2722)
    at org.scalatest.tools.Runner$.runOptionallyWithPassFailReporter(Runner.scala:1043)
    at org.scalatest.tools.Runner$.run(Runner.scala:883)
    at org.scalatest.tools.Runner.run(Runner.scala)
    at org.jetbrains.plugins.scala.testingSupport.scalaTest.ScalaTestRunner.runScalaTest2(ScalaTestRunner.java:138)
    at org.jetbrains.plugins.scala.testingSupport.scalaTest.ScalaTestRunner.main(ScalaTestRunner.java:28)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)

What is interesting, is that it will behave as expected if I make any of the following changes:

Upvotes: 4

Views: 1828

Answers (1)

Michael Zajac
Michael Zajac

Reputation: 55569

The problem is delayed evaluation from Stream/Iterable. This is very easy to see if you break it up into smaller pieces.

scala> Stream("A", "").toIterable.map(_.head)
res7: Iterable[Char] = Stream(A, ?)

Mapping the Stream to _.head means we're going to treat the strings as collections, and the empty string as a collection has no head, so it will throw an exception. But it doesn't throw an exception right away, because an Iterable is still lazy like Stream, as you can see above. Only the first element as evaluated. If we make it non-lazy, it throws the exception:

scala> Stream("A", "").toIterable.map(_.head)
java.util.NoSuchElementException: next on empty iterator

Or, if you replace the "A" with "", you will also get an exception, because the head of the Stream is evaluated and mapped to head, which has none.

scala> Stream("", "").toIterable.map(_.head).toList
java.util.NoSuchElementException: next on empty iterator

The only exception that will be caught in your code is when the head of the Stream or Iterable throws it. Okay, so then why does map not throw the exception, but flatMap does? Well, this map doesn't actually do anything to the Stream:

scala> List(1).map { _ => Stream("A", "").toIterable.map(_.head) }
res11: List[Iterable[Char]] = List(Stream(A, ?))

So it is left untouched, with unevaluated members. But flatMap will try to flatten the Iterables into one collection, so they must be evaluated to make a new collection. And in doing so, the exception is thrown:

scala> List(1).flatMap { _ => Stream("A", "").toIterable.map(_.head) }
java.util.NoSuchElementException: next on empty iterator

Upvotes: 8

Related Questions