Free Handbook · Every example compiled & verified

Errors & Debugging

Read Scala compiler errors and JVM stack traces calmly, fix the 15 errors every Scala beginner hits, and debug with println, tap, the REPL and a real debugger.

0 / 142 lessons🔥 0 day streak
ShareXLinkedIn

Module 11 · what you'll be able to do

  • Tell a compile error from a runtime exception and read each part of a Scala 3 compiler message
  • Read a JVM stack trace to find the line in your own code that caused it
  • Recognise and fix the 15 most common Scala errors, from type mismatch to StackOverflowError
  • Debug with println, tap, the Scala REPL, scala-cli and the IntelliJ or Metals debugger
  • Turn on compiler flags that catch bugs before they run
01

Compile errors vs runtime exceptions

Scala programs fail in two very different ways. A compile error stops the program before it starts: the compiler has proved something is wrong, points at the exact line and column, and nothing runs. A runtime exception happens while the program is running: the code was valid, but some value — an empty list, a null from Java, bad input — made an operation impossible, and the JVM prints a stack trace. Scala's type system is designed to move as many bugs as possible from the second group into the first.

Compile error

  • Printed by the compiler, starts with -- [E007] Type Mismatch Error: or similar
  • Ends with 1 error found / Compilation failed
  • Nothing ran — no output at all
  • Points at a file, line and column, with a caret under the problem
  • Fix it and it is gone for good

Runtime exception

  • Printed by the JVM, starts with Exception in thread "main"
  • Followed by a stack trace: lines starting with at
  • Earlier output already printed; the crash happened part way through
  • Depends on the data: the same code may work on other input
  • Fix the cause, then make the bad case a value (Option, Either)

Anatomy of a Scala 3 compiler message

text
-- [E007] Type Mismatch Error: Main.scala:2:17
2 |  val age: Int = "42"
  |                 ^^^^
  |                 Found:    ("42" : String)
  |                 Required: Int
  |
  | longer explanation available when compiling with `-explain`
1 error found
Compilation failed

The exact output of compiling a two-line program that assigns a String to an Int.

PartMeaning
[E007]The error code. Search "Scala 3 E007" to find the documented explanation.
Type Mismatch ErrorThe category: type mismatch, not found, syntax, cyclic, type error...
Main.scala:2:17File, line 2, column 17. Editors turn this into a clickable link.
The code line and ^^^^The exact expression the compiler objects to.
Found / RequiredWhat you gave versus what that position needs. ("42" : String) means "the literal "42", of type String".
-explain hintRecompile with that flag for a long explanation of this error.
bash
scala compile -explain Main.scala
# in sbt, add to build.sbt:  scalacOptions += "-explain"

Ask the compiler to explain every error in detail.

Fix the first error first
One mistake often produces several messages: a mis-indented line can also make every name on that line "Not found". Read the first error, fix it, and recompile. Half the others usually disappear.
02

Reading a JVM stack trace

A stack trace is the list of method calls that were in progress when the exception was thrown. The first line names the exception and its message. Each at line below is one call, newest first: the top is where the exception was thrown, the bottom is where the program started. Reading it is a skill, and the trick is to skip the library lines and find the frames from your file.

text
Exception in thread "main" java.lang.NumberFormatException: For input string: "two"
	at java.base/java.lang.NumberFormatException.forInputString(Unknown Source)
	at java.base/java.lang.Integer.parseInt(Unknown Source)
	at java.base/java.lang.Integer.parseInt(Unknown Source)
	at scala.collection.StringOps$.toInt$extension(StringOps.scala:971)
	at Main$package$.parseQty(Main.scala:3)
	at Main$package$.totalQty$$anonfun$1(Main.scala:5)
	at scala.collection.immutable.List.map(List.scala:248)
	at Main$package$.totalQty(Main.scala:5)
	at Main$package$.run(Main.scala:9)
	at run.main(Main.scala:7)

Real output of a program whose second order has the quantity "two".

  1. 1
    Read the first line

    The exception type and message are the most useful part: NumberFormatException: For input string: "two" already tells you a string that is not a number was parsed.

  2. 2
    Read the stack bottom-up to see the story

    run.main started the program, run (line 9) called totalQty, which called map, whose lambda called parseQty (line 3). That is the path from your entry point to the crash.

  3. 3
    Find the top-most frame in your file

    Skip java.base/... and scala.collection... frames — the bug is almost never in the JDK. The highest Main.scala line, parseQty(Main.scala:3), is where your code handed over the bad value.

  4. 4
    Decode the Scala names

    Top-level definitions live in a class called Main$package$. $$anonfun$1 is a lambda. $extension is an extension method. A @main def run is launched by a generated class named run.

  5. 5
    Look for "Caused by:"

    When a library wraps an exception, the trace continues with Caused by: .... The last "Caused by" is usually the real root cause. Spark and database drivers do this a lot.

A stack trace is just data on the exception object. You can inspect it yourself — handy in a log line — by filtering getStackTrace to frames from your own file:

scalaMain.scala
case class Order(id: Int, qty: String)

def parseQty(o: Order): Int = o.qty.toInt

def totalQty(orders: List[Order]): Int = orders.map(parseQty).sum

@main def run(): Unit =
  val orders = List(Order(1, "3"), Order(2, "two"))
  try println(totalQty(orders))
  catch
    case e: NumberFormatException =>
      println(s"${e.getClass.getSimpleName}: ${e.getMessage}")
      e.getStackTrace
        .filter(f => f.getFileName != null && f.getFileName.equalsIgnoreCase("Main.scala"))
        .foreach(f => println(s"  at ${f.getMethodName} (line ${f.getLineNumber})"))
Outputcompiled & run with real Scala
NumberFormatException: For input string: "two"
  at parseQty (line 3)
  at totalQty$$anonfun$1 (line 5)
  at totalQty (line 5)
  at run (line 9)
  at main (line 7)

The same five frames the full trace showed, without the library noise. Notice the message does not say which order was bad — the next step in a real fix is to include the order id in the error.

Your turn

Change parseQty to throw IllegalArgumentException(s"order ${o.id}: bad qty '${o.qty}'") when toIntOption is None, and catch that instead.

VisualizeWalking the trace bottom-upStep 1 / 5
case class Order(id: Int, qty: String)
def parseQty(o: Order): Int = o.qty.toInt
def totalQty(orders: List[Order]): Int = orders.map(parseQty).sum
@main def run(): Unit =
val orders = List(Order(1, "3"), Order(2, "two"))
println(totalQty(orders))
Line 7

Bottom frame run.main(Main.scala:7): the launcher calls the @main method.

Variables now

nothing yet

All 5 steps as a table
StepLineWhat happenedVariables now
17Bottom frame run.main(Main.scala:7): the launcher calls the @main method.
29Main$package$.run(Main.scala:9): run calls totalQty.orders = List(Order(1,3), Order(2,two))
35totalQty(Main.scala:5) calls List.map, which calls the lambda totalQty$$anonfun$1 once per order.current = Order(1,3)
43First order: "3".toInt is 3. Fine.current = Order(1,3) result = 3
53Second order: "two".toInt calls Integer.parseInt, which throws. This line is the top-most frame from your file: the place to fix.current = Order(2,two)
03

The compile errors every beginner hits

These eight are the compile errors that fill a Scala beginner's first month. Every message below is real output from Scala 3.9. Each one is the compiler catching a bug that would otherwise have reached a user.

Error you will hit

1. Type mismatch: Found String, Required Int

scala
@main def run(): Unit =
  val age: Int = "42"
  println(age + 1)
-- [E007] Type Mismatch Error: Main.scala:2:17
2 |  val age: Int = "42"
  |                 ^^^^
  |                 Found:    ("42" : String)
  |                 Required: Int
1 error found
Compilation failed
Why the compiler said that

Scala never converts between String and Int for you. The declared type says Int; the value is a String. The same error appears for Option[Int] where Int is needed, List[String] where List[Int] is needed, and a missing else that makes an if return Unit.

The fix

Convert explicitly — toInt if you are sure, toIntOption if the text may not be a number — or fix the declared type. Read Found and Required as "you gave me X, I need Y here".

scala
@main def run(): Unit =
  val age: Int = "42".toInt
  println(age + 1)
Error you will hit

2. Not found: value (usually a typo or missing import)

scala
@main def run(): Unit =
  val total = 10
  println(totl * 2)
-- [E006] Not Found Error: Main.scala:3:10
3 |  println(totl * 2)
  |          ^^^^
  |          Not found: totl - did you mean total?
1 error found
Compilation failed
Why the compiler said that

The name is not defined anywhere the compiler can see from this line. Causes, in order of frequency: a typo, a missing import (Future, ListBuffer, Try), a name used outside the block that defined it, or a wrong case (List vs list).

The fix

Take the "did you mean" suggestion if there is one. Otherwise add the import — scala.util.Try, scala.collection.mutable.ListBuffer, scala.concurrent.Future — or move the definition to where it is used.

scala
@main def run(): Unit =
  val total = 10
  println(total * 2)
Error you will hit

3. value X is not a member of Y

scala
case class User(name: String, email: String)

@main def run(): Unit =
  val u = User("Asha", "[email protected]")
  println(u.emial)
-- [E008] Not Found Error: Main.scala:5:12
5 |  println(u.emial)
  |          ^^^^^^^
  |          value emial is not a member of User - did you mean u.email?
1 error found
Compilation failed
Why the compiler said that

The type on the left (User) has no member with that name. Besides typos, this is what you see when the value is wrapped: value name is not a member of Option[User] means you have an Option and must map over it, and "not a member of Future[...]" means the value has not arrived yet.

The fix

Check the type of the thing before the dot (hover over it in your editor). If it is a wrapper, work inside it with map; if it is a typo, fix it.

scala
case class User(name: String, email: String)

@main def run(): Unit =
  val u = User("Asha", "[email protected]")
  println(u.email)
Error you will hit

4. Missing argument list: must be called with ()

scala
class Clock:
  def now(): Long = 42L

@main def run(): Unit =
  val c = Clock()
  val t: Long = c.now
  println(t)
-- [E100] Syntax Error: Main.scala:6:18
6 |  val t: Long = c.now
  |                ^^^^^
  |                method now in class Clock must be called with () argument
1 error found
Compilation failed
Why the compiler said that

Scala 3 is strict about parentheses: a method declared with an empty argument list () must be called with one. Scala 2 let you drop them, so older tutorials and Stack Overflow answers often show c.now. The convention: () signals the method has a side effect or reads changing state; no parentheses means a pure, property-like value.

The fix

Call it as declared, c.now(). The related case — a curried method called with only its first argument list — is reported as a type mismatch: Found: String => String, Required: String, which means "you still owe me an argument list".

scala
class Clock:
  def now(): Long = 42L

@main def run(): Unit =
  val c = Clock()
  val t: Long = c.now()
  println(t)
Error you will hit

5. Reassignment to val

scala
@main def run(): Unit =
  val count = 0
  count = count + 1
  println(count)
-- [E052] Type Error: Main.scala:3:8
3 |  count = count + 1
  |  ^^^^^^^^^^^^^^^^^
  |  Reassignment to val count
1 error found
Compilation failed
Why the compiler said that

A val is fixed once assigned (see Module 01). The compiler refuses any = to it later, which is the point: readers never need to track how a val changes.

The fix

Usually, do not mutate at all — compute the new value into a new val, or use foldLeft for an accumulating loop. If a local counter really needs to change, declare it var.

scala
@main def run(): Unit =
  var count = 0
  count = count + 1
  println(count)
Error you will hit

6. Recursive method needs a result type

scala
def factorial(n: Int) =
  if n <= 1 then 1 else n * factorial(n - 1)

@main def run(): Unit =
  println(factorial(5))
-- [E044] Cyclic Error: Main.scala:2:28
2 |  if n <= 1 then 1 else n * factorial(n - 1)
  |                            ^
  |                Overloaded or recursive method factorial needs return type
  |
  |                 Run with -explain-cyclic for more details.
1 error found
Compilation failed
Why the compiler said that

To infer the result type of factorial, the compiler must know the type of its body, which contains a call to factorial — whose type it is still working out. That circle has no starting point, so a recursive method must state its result type.

The fix

Write the result type. It is good practice on every public method anyway: the signature becomes documentation and errors appear where you made them.

scala
def factorial(n: Int): Int =
  if n <= 1 then 1 else n * factorial(n - 1)

@main def run(): Unit =
  println(factorial(5))
Error you will hit

7. Indentation does not match

scala
@main def run(): Unit =
  val scores = List(70, 85, 92)
  for s <- scores do
      val grade = if s >= 90 then "A" else "B"
    println(s"$s -> $grade")
-- Error: Main.scala:5:4
5 |    println(s"$s -> $grade")
  |    ^
  |The start of this line does not match any of the previous indentation widths.
  |Indentation width of current line : 4 spaces
  |This falls between previous widths: 2 spaces and 6 spaces
-- [E006] Not Found Error: Main.scala:5:15
5 |    println(s"$s -> $grade")
  |               ^
  |               Not found: s
-- [E006] Not Found Error: Main.scala:5:21
5 |    println(s"$s -> $grade")
  |                     ^^^^^
  |                     Not found: grade
3 errors found
Compilation failed
Why the compiler said that

With Scala 3's braceless syntax (Module 02), indentation is the block structure. The body of the for started at 6 spaces; the next line at 4 matches neither the body (6) nor the enclosing block (2), so the compiler cannot tell where it belongs. The two "Not found" errors are knock-on effects: once the line falls out of the loop, s and grade are out of scope.

The fix

Indent every line of a block to the same column (two spaces per level is the convention) and let your editor or scalafmt format the file. Fix the first error; the others vanish with it.

scala
@main def run(): Unit =
  val scores = List(70, 85, 92)
  for s <- scores do
    val grade = if s >= 90 then "A" else "B"
    println(s"$s -> $grade")
Error you will hit

8. No given instance of type Ordering[...]

scala
case class Employee(name: String, salary: Int)

def highestPaid[A: Ordering](xs: List[A]): A = xs.max

@main def run(): Unit =
  val staff = List(Employee("Ravi", 50), Employee("Asha", 70))
  println(highestPaid(staff))
-- [E172] Type Error: Main.scala:7:28
7 |  println(highestPaid(staff))
  |                            ^
  |No given instance of type Ordering[Employee] was found for a context parameter of method highestPaid.
  |I found:
  |
  |    scala.math.Ordering.ordered[Employee](
  |      /* missing */summon[scala.math.Ordering.AsComparable[Employee]])
  |
  |But no implicit values were found that match type scala.math.Ordering.AsComparable[Employee].
  |
  |The following import might make progress towards fixing the problem:
  |
  |  import scala.math.Ordered.orderingToOrdered
  |
1 error found
Compilation failed
Why the compiler said that

[A: Ordering] asks for a given Ordering[A] (Module 09). Scala knows how to order Int and String, but not your Employee — should it compare by name or by salary? The long "I found" part is the compiler describing what it tried; the suggested import will not help here. You see the same error from staff.sorted, staff.max and staff.sum.

The fix

Say how to order the type with a given, or skip the question by sorting on a field: staff.maxBy(_.salary).

scala
case class Employee(name: String, salary: Int)

given Ordering[Employee] = Ordering.by(_.salary)

def highestPaid[A: Ordering](xs: List[A]): A = xs.max

@main def run(): Unit =
  val staff = List(Employee("Ravi", 50), Employee("Asha", 70))
  println(highestPaid(staff))
04

Pattern matching: the warning and the crash

A match that misses a case is the one bug Scala reports as a warning rather than an error: the program still compiles and runs. If the missing case ever arrives, it crashes with MatchError. The two cards below are the same bug seen at compile time and at runtime. Treat the warning as an error — many teams make it one with -Werror.

Error you will hit

9. match may not be exhaustive

scala
enum Status:
  case Active, Suspended, Closed

def label(s: Status): String = s match
  case Status.Active    => "active"
  case Status.Suspended => "on hold"

@main def run(): Unit =
  println(label(Status.Active))
-- [E029] Pattern Match Exhaustivity Warning: Main.scala:4:31
4 |def label(s: Status): String = s match
  |                               ^
  |                               match may not be exhaustive.
  |
  |                               It would fail on pattern case: Closed
1 warning found
active
Why the compiler said that

Because Status is an enum (or a sealed trait, Module 05), the compiler knows every possible case and can see that Closed is not handled. It still builds the program — note active printed after the warning — because the missing case might never occur. The day someone closes an account, it will.

The fix

Handle every case. This is why you should avoid a catch-all case _ on your own enums: without it, adding a new case later makes the compiler list every match you need to update.

scala
enum Status:
  case Active, Suspended, Closed

def label(s: Status): String = s match
  case Status.Active    => "active"
  case Status.Suspended => "on hold"
  case Status.Closed    => "closed"

@main def run(): Unit =
  println(label(Status.Active))
Error you will hit

10. scala.MatchError

scala
def describe(code: Int): String = code match
  case 200 => "OK"
  case 404 => "Not Found"

@main def run(): Unit =
  println(describe(200))
  println(describe(500))
OK
Exception in thread "main" scala.MatchError: 500 (of class java.lang.Integer)
	at Main$package$.describe(Main.scala:3)
	at Main$package$.run(Main.scala:7)
	at run.main(Main.scala:5)
Why the compiler said that

Matching on an Int, the compiler cannot list every value, so there was no warning. At runtime 500 matched no case and Scala threw MatchError, whose message is the value that fell through. The first call printed OK before the crash — typical of runtime exceptions.

The fix

Add a final case for everything else. Better still, when the input really has a fixed set of values, model it as an enum so the compiler can check exhaustiveness for you.

scala
def describe(code: Int): String = code match
  case 200   => "OK"
  case 404   => "Not Found"
  case other => s"HTTP $other"

@main def run(): Unit =
  println(describe(200))
  println(describe(500))
05

The runtime exceptions you will meet

Each of these compiles cleanly and crashes when it runs. Four of the five come from the same root cause: code that assumes a value is there, is the right type, or is a number, without checking. The fixes use the tools from Module 08.

Error you will hit

11. NoSuchElementException: head of empty list

scala
@main def run(): Unit =
  val queue = List.empty[String]
  val next = queue.head
  println(s"processing $next")
Exception in thread "main" java.util.NoSuchElementException: head of empty list
	at scala.collection.immutable.Nil$.head(List.scala:664)
	at scala.collection.immutable.Nil$.head(List.scala:664)
	at Main$package$.run(Main.scala:3)
	at run.main(Main.scala:1)
Why the compiler said that

head means "the first element, I am certain there is one". On an empty list there is none. The same exception comes from None.get (message None.get), last, max and reduce on an empty collection, and Map.apply with a missing key (key not found: ...).

The fix

Use the safe versions that return an Option — headOption, lastOption, maxOption, reduceOption, map.get(key) — and decide what an empty input means.

scala
@main def run(): Unit =
  val queue = List.empty[String]
  queue.headOption match
    case Some(next) => println(s"processing $next")
    case None       => println("queue is empty")
Error you will hit

12. NullPointerException from a Java API

scala
@main def run(): Unit =
  val mode: String = System.getProperty("app.mode")
  println(mode.toUpperCase)
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "mode" is null
	at Main$package$.run(Main.scala:3)
	at run.main(Main.scala:1)
Why the compiler said that

Idiomatic Scala never uses null, but Java libraries do: System.getProperty, System.getenv, HashMap.get, JDBC's getString and many more return null for "nothing". The type still says String, so the compiler cannot help. The JVM's helpful message names the variable that was null.

The fix

Wrap every Java value that might be null in Option(...) at the boundary, as soon as it enters your code. From then on the type tells the truth.

scala
@main def run(): Unit =
  val mode: String = Option(System.getProperty("app.mode")).getOrElse("dev")
  println(mode.toUpperCase)
Error you will hit

13. ClassCastException from asInstanceOf

scala
@main def run(): Unit =
  val values: List[Any] = List(1, "two", 3)
  val total = values.map(_.asInstanceOf[Int]).sum
  println(total)
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
	at scala.runtime.BoxesRunTime.unboxToInt(BoxesRunTime.java:100)
	at Main$package$.$anonfun$1(Main.scala:3)
	at scala.collection.immutable.List.map(List.scala:248)
	at Main$package$.run(Main.scala:3)
	at run.main(Main.scala:1)
Why the compiler said that

asInstanceOf tells the compiler "trust me, this is an Int" and switches off the type check. The compiler trusted you; the JVM checked at runtime and found a String. A List[Any] is usually the real problem — the types were lost earlier, often when parsing JSON or reading a Java collection.

The fix

Use a type pattern, which checks and converts in one safe step: collect { case n: Int => n } or a match. Better, keep precise types from the start so you never hold a List[Any].

scala
@main def run(): Unit =
  val values: List[Any] = List(1, "two", 3)
  val total = values.collect { case n: Int => n }.sum
  println(total)
Error you will hit

14. NumberFormatException from toInt

scala
@main def run(): Unit =
  val raw = List("10", "20", "3O")
  val numbers = raw.map(_.toInt)
  println(numbers.sum)
Exception in thread "main" java.lang.NumberFormatException: For input string: "3O"
	at java.base/java.lang.NumberFormatException.forInputString(Unknown Source)
	at java.base/java.lang.Integer.parseInt(Unknown Source)
	at java.base/java.lang.Integer.parseInt(Unknown Source)
	at scala.collection.StringOps$.toInt$extension(StringOps.scala:971)
	at Main$package$.$anonfun$1(Main.scala:3)
	at scala.collection.immutable.List.map(List.scala:248)
	at Main$package$.run(Main.scala:3)
	at run.main(Main.scala:1)
Why the compiler said that

The third value is 3O — a letter O, not a zero. toInt delegates to Java's Integer.parseInt, which throws on anything that is not a whole number, including "", " 3" and "3.5". Any text from a file, form or API can contain this.

The fix

Parse with toIntOption and decide what happens to bad values: drop them, default them, or report them with partition or Either.

scala
@main def run(): Unit =
  val raw = List("10", "20", "3O")
  val (good, bad) = raw.partition(_.toIntOption.isDefined)
  println(good.map(_.toInt).sum)
  println(s"rejected: $bad")
Error you will hit

15. StackOverflowError from deep recursion

scala
def sumTo(n: Long): Long =
  if n == 0 then 0 else n + sumTo(n - 1)

@main def run(): Unit =
  println(sumTo(100_000))
Exception in thread "main" java.lang.StackOverflowError
	at Main$package$.sumTo(Main.scala:2)
	at Main$package$.sumTo(Main.scala:2)
	at Main$package$.sumTo(Main.scala:2)
	at Main$package$.sumTo(Main.scala:2)
	...
Why the compiler said that

Each call waits for the next one to return before it can do its n + ..., so 100,000 calls are on the stack at once. The JVM stack holds only a few thousand to tens of thousands of frames, and it runs out. The trace (cut here — the real one repeats the same line 1,024 times) is the giveaway: one frame, over and over.

The fix

Make the recursive call the very last thing the function does by carrying the running total in an accumulator, and add @tailrec so the compiler turns it into a loop and fails the build if it cannot (Module 03). Or use (1L to n).sum.

scala
import scala.annotation.tailrec

def sumTo(n: Long): Long =
  @tailrec def loop(i: Long, acc: Long): Long =
    if i == 0 then acc else loop(i - 1, acc + i)
  loop(n, 0)

@main def run(): Unit =
  println(sumTo(100_000))
06

Debugging tools: println, tap, REPL, debugger

When the program runs but gives the wrong answer, you need to see its values. Start simple and escalate: a println, then the REPL to try a single expression, then a real debugger when you need to pause and look around.

println and tap: look inside a pipeline

Long chains of map and filter are hard to println without breaking them apart. scala.util.chaining adds tap, which runs a side effect on a value and passes the value on unchanged — you can drop it between any two steps and delete it afterwards.

scalaMain.scala
import scala.util.chaining.*

@main def run(): Unit =
  val result = List(5, 3, 8, 1)
    .tap(xs => println(s"input:    $xs"))
    .filter(_ > 2)
    .tap(xs => println(s"filtered: $xs"))
    .map(_ * 10)
    .tap(xs => println(s"mapped:   $xs"))
    .sum
  println(result)
Outputcompiled & run with real Scala
input:    List(5, 3, 8, 1)
filtered: List(5, 3, 8)
mapped:   List(50, 30, 80)
160

Each tap prints the intermediate collection. Remove the lines when you are done — or, in real code, turn them into log statements at debug level.

Your turn

Add .distinct after the filter with a tap after it, and change the input to List(5, 3, 8, 3).

Logging instead of println

scala
//> using dep ch.qos.logback:logback-classic:1.5.18
//> using dep com.typesafe.scala-logging::scala-logging:3.9.5
import com.typesafe.scalalogging.LazyLogging

object OrderService extends LazyLogging:
  def place(id: Int, qty: Int): Unit =
    logger.debug(s"placing order $id qty=$qty")   // only built if DEBUG is on
    if qty <= 0 then logger.warn(s"order $id rejected: qty=$qty")
    else logger.info(s"order $id placed")

In a service, use a logger (SLF4J with Logback, or scala-logging on top of it). Levels let you keep debug lines in the code and switch them on only when needed.

The REPL and scala-cli

bash
$ scala
scala> "3O".toIntOption
val res0: Option[Int] = None

scala> List(1, 2, 3).sliding(2).toList
val res1: List[List[Int]] = List(List(1, 2), List(2, 3))

scala> :type List(1, "a")
List[Int | String]

scala> :quit

# a REPL with your project's file and libraries on the classpath
$ scala repl Main.scala

# rerun on every save while you debug
$ scala run --watch Main.scala

The REPL evaluates one expression at a time and shows its type — the fastest way to check what a method returns.

A real debugger: IntelliJ IDEA and Metals

  1. 1
    Set a breakpoint

    Click in the gutter next to a line number in IntelliJ IDEA (with the Scala plugin) or VS Code (with the Metals extension).

  2. 2
    Start in debug mode

    IntelliJ: the bug icon next to your @main or test. Metals: the debug code lens that appears above @main and each test.

  3. 3
    Inspect

    When execution pauses, the Variables panel shows every local value, including the fields of case classes. Hover over any expression to see its value.

  4. 4
    Step

    Step over runs the current line; step into enters the method it calls; step out finishes the current method. Conditional breakpoints (order.id == 2) stop only on the case you care about.

  5. 5
    Evaluate

    The Evaluate Expression window runs Scala code against the paused program — try a fix before editing the file.

Let the compiler find bugs for you

scalaproject.scala
//> using scala 3.9.0
//> using options -deprecation -feature -Wunused:all -Werror

scala-cli directives (sbt: scalacOptions ++= Seq(...)). -Wunused:all flags unused imports, locals and parameters — often a sign of a mistake. -Werror turns every warning, including non-exhaustive matches, into an error.

In real jobs: debugging Spark and services
On a Spark cluster there is no debugger to attach: you read the driver and executor logs in the Spark UI and scroll to the last Caused by:. For services, structured logs with a request id let you follow one request through every line it touched. Both are the same skill as this lesson — read the trace bottom-up, find your code, check the data. See Spark with Scala for Spark-specific errors.
07

Index of the 15 errors

Bookmark this table. When you meet an error, find its first line here, then jump to the card for the cause and the fix.

#Message starts withWhenUsual causeUsual fix
1[E007] Type Mismatch ErrorCompileValue of the wrong typeConvert explicitly or fix the declared type
2[E006] Not found: xCompileTypo, missing import, out of scopeFix the name or add the import
3value x is not a member of TCompileTypo, or the value is wrapped in Option/FutureCheck the type; map inside the wrapper
4must be called with () argumentCompileDropped () on a method declared with itCall it as m()
5Reassignment to valCompileAssigning to a valNew val, foldLeft, or var
6recursive method ... needs return typeCompileRecursion without a result typeDeclare the result type
7does not match any of the previous indentation widthsCompileInconsistent indentationAlign the block; run scalafmt
8No given instance of type ...CompileMissing type class instance (Ordering, etc.)Define a given or use sortBy/maxBy
9match may not be exhaustiveCompile (warning)A case of an enum or sealed trait not handledHandle every case
10scala.MatchErrorRuntimeA value matched no caseAdd a final case; model inputs as enums
11NoSuchElementExceptionRuntimehead, get, max on emptyheadOption, getOrElse, maxOption
12NullPointerExceptionRuntimenull from a Java APIWrap in Option(...) at the boundary
13ClassCastExceptionRuntimeasInstanceOf on the wrong typeType pattern with collect / match
14NumberFormatExceptionRuntimetoInt on text that is not a numbertoIntOption
15StackOverflowErrorRuntimeDeep non-tail recursionAccumulator + @tailrec
Compile error
A problem the compiler proves before the program runs; nothing executes until it is fixed.
Runtime exception
An error thrown while the program runs, printed with a stack trace; depends on the data.
Stack trace
The chain of method calls in progress when an exception was thrown, newest call first.
Frame
One line of a stack trace: a method, its file and line number.
Caused by
A section of a stack trace showing an exception that was wrapped inside another; the last one is usually the root cause.
-explain
Compiler flag that prints a long explanation for each error.
Exhaustivity check
The compiler's check that a match on an enum or sealed trait handles every case.
MatchError
The runtime exception thrown when a value matches no case of a match expression.
tap
Method from scala.util.chaining that runs a side effect on a value and returns the value unchanged.
Breakpoint
A marked line where a debugger pauses the program so you can inspect its values.
Quick check

A stack trace lists these frames from top to bottom: java.lang.Integer.parseInt, StringOps$.toInt$extension, Main$package$.parseQty(Main.scala:3), Main$package$.run(Main.scala:9). Where do you look first?

Quick check

Which of these is reported by the compiler only as a warning, so the program still runs?

Frequently asked questions

How do I read a Scala stack trace?
Read the first line for the exception type and message, then scan down for the first frame that points at your own file, such as Main.scala:3 — that is where your code passed a bad value into a library. Frames below it show how the program got there. If there is a Caused by: section, the last one is usually the real cause.
What does "match may not be exhaustive" mean in Scala?
You matched on an enum or sealed trait but did not handle every case. The code still compiles, but it will throw scala.MatchError if an unhandled case arrives. Add the missing cases, and avoid a catch-all case _ so the compiler can warn you when new cases are added.
How do I fix a StackOverflowError in Scala?
It almost always comes from recursion that goes too deep. Rewrite the function so the recursive call is the last thing it does, carrying partial results in an accumulator parameter, and mark it @tailrec. The compiler then compiles it into a loop that uses constant stack space.

Finish the Scala handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.