Compiler Options, Linting & Errors

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Scala 3.10 (3.10.0, released 2026-10-12). Every command and every piece of compiler output below was produced by running the 3.10.0 compiler on JDK 26 through Scala CLI (checked on 2026-10-15); the ANSI colour codes were stripped from the output and the long absolute paths were shortened. Options that are newer than the 3.3 LTS say so: -opt arrived in 3.8.3, -preview in 3.7, -Xmax-fuel and the fuel-based recursion limit in 3.10, and the // error and // warn markers for documentation snippets in 3.9 (see Scaladoc). The 3.9 LTS has every option shown here except -Xmax-fuel; where a result was also checked on 3.9.0 the text says so.

The Scala compiler is deliberately quiet by default. A fresh project compiles with a handful of "there was 1 deprecation warning; re-run with -deprecation for details" summary lines and nothing else, so the checks that Java developers get from javac -Xlint:all or Kotlin developers get from the IDE have to be asked for. This page is the catalogue of what to ask for, how to turn a warning into an error without breaking the build every time a library is upgraded, how to read the error messages and their codes, and how to use the compiler to migrate code between language versions.

Where options are set

The same options are accepted everywhere; only the spelling of the place changes.

Tool Where Example

Scala CLI, on the command line

any extra argument that starts with a dash is passed to scalac

scala compile . -Wunused:all

Scala CLI, in the source file

a //> using options directive, which is the form to commit

//> using options -deprecation -feature -Wunused:all -Werror

sbt

scalacOptions in build.sbt

scalacOptions ++= Seq("-deprecation", "-feature", "-Wunused:all")

The compiler launcher from the distribution

plain arguments

scalac -Wunused:all -d out src/Trail.scala

sbt and the scalac launcher were not exercised for the output below: every example uses Scala CLI, whose options reach the same compiler. The scalacOptions line is the standard sbt form from the reference documentation (sbt is covered in sbt & Build Tools, Scala CLI in Scala CLI).

Options are grouped by prefix, and each group has a help form that prints exactly the choices of the compiler you are running, which is the authority when this page and your version disagree.

Prefix Meaning

- plain

standard options: -deprecation, -feature, -unchecked, -explain, -source, -rewrite, -preview, -experimental, -language, -opt, -nowarn, -no-indent, -new-syntax, -old-syntax, -java-output-version

-W

warnings: -Wunused, -Wvalue-discard, -Wnonunit-statement, -Wshadow, -Wsafe-init, -Wall, -Werror, -Wconf, -Wtostring-interpolated, -Wrecurse-with-default, -Winfer-union, -Wimplausible-patterns, -Wopt, -Wwrong-arrow, -Wenum-comment-discard, -WunstableInlineAccessors

-X

advanced settings that are supported: -Xmax-fuel, -Xmax-inlines, -Xfatal-warnings

-V

verbose and debugging output

-Y

private settings for compiler work and experiments, not stable between releases

$ scala compile . -Wunused:help
-Wunused  Enable or disable specific `unused` warnings
          Choices:
          - nowarn,
          - all,
          - imports :
          	Warn if an import selector is not referenced.,
          - privates :
          	Warn if a private member is unused,
          - locals :
          	Warn if a local definition is unused,
          - explicits :
          	Warn if an explicit parameter is unused,
          - implicits :
          	Warn if an implicit parameter is unused,
          - params :
          	Enable -Wunused:explicits,implicits,
          - patvars :
          	Warn if a variable bound in a pattern is unused,
          - linted :
          	Enable -Wunused:imports,privates,locals,implicits

Deprecation, feature and unchecked warnings

Three warning families are off by default and are summarised in a single line instead of being printed. This is a file with one use of each of the first two:

//> using scala 3.10.0
package trailhead

@deprecated("use newName", "1.2")
def oldName(): Int = 1

given Conversion[Int, String] = _.toString

@main def dep(): Unit =
  val n = oldName()
  val s: String = n
  println(s)

Compiled with no options the compiler counts the warnings and stops there:

$ scala compile .
there was 1 feature warning; re-run with -feature for details
there was 1 deprecation warning; re-run with -deprecation for details
2 warnings found

With -deprecation -feature the messages and the exact positions appear:

$ scala compile . -deprecation -feature
-- Feature Warning: c/Dep.scala:11:18
11 |  val s: String = n
   |                  ^
   |Use of implicit conversion given instance given_Conversion_Int_String in package trailhead should be enabled
   |by adding the import clause 'import scala.language.implicitConversions'
   |or by setting the compiler option -language:implicitConversions.
   |See the Scala docs for value scala.language.implicitConversions for a discussion
   |why the feature should be explicitly enabled.
-- Deprecation Warning: c/Dep.scala:10:10
10 |  val n = oldName()
   |          ^^^^^^^
   |method oldName in package trailhead is deprecated since 1.2: use newName
2 warnings found

-feature reports the language features that must be enabled explicitly (implicit conversions, postfixOps, dynamics, adhocExtensions and similar). The remedy is a import scala.language.implicitConversions at the use site, which documents the decision in the file, or the project-wide -language:implicitConversions option, which does not. Prefer the import. -language:help lists every feature name, and the same flag is also where the experimental.* imports are enabled (see Preview and experimental).

-unchecked reports type tests that cannot be verified at run time because of erasure. In 3.10 the main cases already warn without the flag:

//> using scala 3.10.0
package trailhead

def kind(x: Any): String = x match
  case xs: List[String] => "strings"
  case _ => "other"

@main def unch(): Unit = println(kind(List(1)))
-- [E092] Pattern Match Unchecked Warning: q/Unch.scala:5:7
5 |  case xs: List[String] => "strings"
  |       ^
  |the type test for List[String] cannot be checked at runtime because its type arguments can't be determined from Any
  |
  | longer explanation available when compiling with `-explain`
1 warning found

Unused code: -Wunused

-Wunused takes a comma-separated list of choices. The table below pairs each choice with the message it produces in the sample that follows.

Choice Warns about Message

imports

an import selector that is never referenced

unused import

privates

a private field or method that is never used

unused private member

locals

a local val, var or def that is never used

unused local definition

explicits

an explicit parameter that the body ignores

unused explicit parameter

implicits

a using or implicit parameter that is never needed

unused implicit parameter

params

both of the two above

patvars

a name bound in a pattern and never used

unused pattern variable

linted

imports,privates,locals,implicits

all

every choice, including patvars and explicits

nowarn

a @nowarn annotation that suppresses nothing

@nowarn annotation does not suppress any warnings

//> using scala 3.10.0
package trailhead

import java.util.Date
import scala.collection.mutable.ListBuffer

class Counter:
  private var hidden = 0
  private def unusedHelper(): Int = 1
  def next(x: Int, unusedParam: Int): Int =
    val local = 5
    x + 1

@main def lint(): Unit =
  println(Counter().next(1, 2))
  def sideEffect(): Int = 42
  def useIt(): Unit = sideEffect()
  useIt()
  (1, 2) match
    case (a, b) => println(a)
$ scala compile . -Wunused:all
-- [E198] Unused Symbol Warning: a/Lint.scala:4:17
4 |import java.util.Date
  |                 ^^^^
  |                 unused import
-- [E198] Unused Symbol Warning: a/Lint.scala:5:32
5 |import scala.collection.mutable.ListBuffer
  |                                ^^^^^^^^^^
  |                                unused import
-- [E198] Unused Symbol Warning: a/Lint.scala:8:14
8 |  private var hidden = 0
  |              ^^^^^^
  |              unused private member
-- [E198] Unused Symbol Warning: a/Lint.scala:9:14
9 |  private def unusedHelper(): Int = 1
  |              ^^^^^^^^^^^^
  |              unused private member
-- [E198] Unused Symbol Warning: a/Lint.scala:10:19
10 |  def next(x: Int, unusedParam: Int): Int =
   |                   ^^^^^^^^^^^
   |                   unused explicit parameter
-- [E198] Unused Symbol Warning: a/Lint.scala:11:8
11 |    val local = 5
   |        ^^^^^
   |        unused local definition
-- [E198] Unused Symbol Warning: a/Lint.scala:20:13
20 |    case (a, b) => println(a)
   |             ^
   |             unused pattern variable
7 warnings found

-Wunused:linted reports five of those seven (it leaves out the parameter and the pattern variable), and -Wunused:patvars on its own reports only the last. The usual project setting is -Wunused:all, which is also what -Wall includes. -Wunused was rewritten in 3.7 and produces far fewer false positives than the 3.3 LTS implementation did, so a codebase that gave up on it in the past deserves a second try.

An unused parameter that exists because an override or a callback must have that signature is silenced by naming it _, or with @unused from scala.annotation (no warning under -Wall), rather than by turning the whole check off.

Discarded values: -Wvalue-discard and -Wnonunit-statement

Two checks look at expressions whose result is thrown away. They are different in what they report:

  • -Wvalue-discard warns when a non-Unit value is converted to Unit only because the expected type is Unit, the way def f(): Unit = g() silently drops `g()’s result.

  • -Wnonunit-statement warns when a statement in a block has a non-Unit type and its value is dropped.

//> using scala 3.10.0
package trailhead

import scala.collection.mutable.ListBuffer

def sideEffect(): Int = 42

def useIt(): Unit = sideEffect()

@main def discard(): Unit =
  val buf = ListBuffer(1, 2)
  buf.remove(0)
  buf += 3
  useIt()
  println(buf)
$ scala compile . -Wvalue-discard
-- [E175] Potential Issue Warning: b/Discard.scala:8:30
8 |def useIt(): Unit = sideEffect()
  |                    ^^^^^^^^^^^^
  |   discarded non-Unit value of type Int. Add `: Unit` to discard silently.
1 warning found

$ scala compile . -Wnonunit-statement
-- [E176] Potential Issue Warning: b/Discard.scala:12:12
12 |  buf.remove(0)
   |  ^^^^^^^^^^^^^
   |  unused value of type Int
1 warning found

buf += 3 is not reported by -Wnonunit-statement because += returns this.type, a deliberately exempted builder-style result. The silent discard is written sideEffect(): Unit (a type ascription) for the first check and val _ = buf.remove(0) for the second; both forms compile cleanly under -Wall -Werror. Both checks catch the classic bug of a Future, an Either or a Try that is created and never looked at; -Wnonunit-statement is the one that finds it in a block.

Other lint options

Option What it checks

-Wshadow:all

a constructor parameter that shadows an inherited field (private-shadow), and a type parameter that shadows a type in scope (type-parameter-shadow)

-Wtostring-interpolated

a standard interpolator that calls toString on a reference type

-Wrecurse-with-default

a method that calls itself and relies on a default argument

-Winfer-union

a type argument that the compiler inferred as a union, which often hides a mistake such as List(1, "two")

-Wimplausible-patterns

a pattern whose comparison looks like it can never succeed

-Wwrong-arrow

=> used where the context-function arrow ?=> was meant

-Wsafe-init

the safe initialization check (see below)

-Wopt

optimizer diagnostics, see The optimizer: -opt (3.8.3)

-Wall

every warning setting that is not a configuration option

The first four of those, run on one small file with the trailhead types, produce these messages:

$ scala compile . -Wshadow:all
-- [E227] Naming Warning: m/Misc.scala:7:14
7 |class Derived(len: Int) extends Base(len):
  |              ^
  |    value len in class Derived shadows field len inherited from class Base
  |
  | longer explanation available when compiling with `-explain`
1 warning found

$ scala compile . -Wtostring-interpolated
-- [E209] Interpolation Warning: m/Misc.scala:15:43
15 |def describe(t: Trail): String = s"trail: $t"
   |                                           ^
   |                                           interpolation uses toString
1 warning found

$ scala compile . -Wrecurse-with-default
-- [E221] Type Warning: m/Misc.scala:13:33
13 |  if n <= 0 then 0 else countDown(n - step)
   |                        ^^^^^^^^^^^^^^^^^^^
   |              Recursive call used a default argument for parameter step.
   |
   | longer explanation available when compiling with `-explain`
1 warning found

$ scala compile . -Winfer-union
-- [E225] Type Warning: n/U.scala:5:14
5 |  val mixed = List(1, "two")
  |              ^^^^
  |              A type argument was inferred to be union type Int | String
  |              This may indicate a programming error.
1 warning found

-Wsafe-init

Scala initializes `val`s in textual order, so a superclass constructor that calls an overridable method can observe a subclass field before it is assigned. The program below compiles cleanly and prints a wrong answer:

//> using scala 3.10.0
package trailhead

abstract class Base:
  println(describe())
  def describe(): String

class Trail(val name: String) extends Base:
  val length: Int = name.length
  def describe(): String = s"$name has $length"

@main def init(): Unit = Trail("ridge")
$ scala run .
ridge has 0

-Wsafe-init runs the compiler’s initialization checker and reports the access with a calling trace:

$ scala compile . -Wsafe-init
-- Warning: e/Init.scala:9:6
9 |  val length: Int = name.length
  |      ^
  |      Access non-initialized value length. Calling trace:
  |      ├── class Trail(val name: String) extends Base:	[ Init.scala:8 ]
  |      │   ^
  |      ├── abstract class Base:	[ Init.scala:4 ]
  |      │   ^
  |      ├── println(describe())	[ Init.scala:5 ]
  |      │           ^^^^^^^^^^
  |      └── def describe(): String = s"$name has $length"	[ Init.scala:10 ]
  |                                                ^^^^^^
1 warning found

The trace reads from the constructor of Trail into the constructor of Base, into the call of describe, and ends on the read of the field that has not been assigned yet. Java has the same trap and no compiler check for it; Kotlin reports the simple cases.

-Wall

-Wall turns on all of the settings above that take no argument and uses all for the ones that do. On the files used in this page it finds exactly what the individual flags found: eight warnings on the lint sample (the seven unused-code warnings plus the discarded Int), two on the discard sample, three on the shadowing sample (shadowing, toString in an interpolation, default argument in a recursive call), the type-argument warning on the union sample, and the initialization warning (plus a discarded value) on the initialization sample. It is the right default for a new project and a noisy one for an old code base, where the individual flags are the way to adopt the checks one at a time.

Making warnings fatal: -Werror and -Wconf

-Werror fails the compilation if there is any warning. Scala 3 spells it that way; the Scala 2 name -Xfatal-warnings is still accepted (the compiler gave the same result with it), whereas -Xlint and -Ywarn-unused are not options of the Scala 3 compiler and are ignored with a bad option '-Ywarn-unused' was ignored warning.

$ scala compile . -deprecation -feature -Werror
-- Feature Warning: c/Dep.scala:11:18
...
-- Deprecation Warning: c/Dep.scala:10:10
...
No warnings can be incurred under -Werror
2 warnings found
1 error found
Compilation failed

The output is the same pair of messages as before (shortened here), followed by the summary line, and the compiler exits with status 1. Fatal warnings are right for a CI build and wrong for a developer build, because a library upgrade can add a deprecation and stop everyone’s compilation. -Wconf is the tool that makes the policy selective.

The -Wconf syntax

-Wconf is a list of filter:action rules separated by commas; several filters in one rule are combined with &. The compiler help lists the filters and the actions.

Filter Matches

any

every message

cat=deprecation, cat=feature, cat=unchecked

a message category

msg=regex

part of the message text

id=E198

the message id, which is printed in the heading of each warning

name=PureExpressionInStatementPosition

the message name, shown in verbose mode

src=regex

the full path of the source file

origin=regex

the full name (package.Class.method) of the deprecated entity

Action Effect

error or e

report as an error

warning or w

report as a warning

verbose or v

report as a warning and print the filters that matched

info or i

report as information; not counted by -Werror

silent or s

do not report

The rule that decides everything is "the last matching configuration, in definition order, determines the action".

$ scala compile . -deprecation -feature -Wconf:any:verbose
-- Feature Warning: c/Dep.scala:11:18
...
   |Matching filters for @nowarn or -Wconf:
   |  - cat=feature
-- Deprecation Warning: c/Dep.scala:10:10
10 |  val n = oldName()
   |          ^^^^^^^
   |method oldName in package trailhead is deprecated since 1.2: use newName
   |Matching filters for @nowarn or -Wconf:
   |  - cat=deprecation
   |  - origin=trailhead.oldName
2 warnings found

Verbose mode gives you the filters to paste. Silence one category and keep failing on the rest:

$ scala compile . -deprecation -feature -Wconf:cat=deprecation:s -Werror
-- Feature Warning: c/Dep.scala:11:18
...
No warnings can be incurred under -Werror
1 warning found
1 error found
Compilation failed

Silencing a single message id is how a legacy warning is parked while the rest of the lint set stays strict. On the unused code sample, -Wunused:all -Wconf:id=E198:s -Werror compiles without any output, because every E198 warning is silenced before -Werror sees it. The order matters: with "-Wconf:msg=unused import:s,id=E198:e" the second rule matches the same messages later and wins, and all seven unused warnings become errors (7 errors found), imports included.

The @nowarn annotation does the same job at one declaration, with the same filter syntax, which keeps a suppression next to the code that needs it:

//> using scala 3.10.0
package trailhead

import scala.annotation.nowarn

@deprecated("use newName", "1.2")
def oldName(): Int = 1

@nowarn("cat=deprecation")
def quiet: Int = oldName()

@nowarn
def unneeded: Int = 1

@main def nw(): Unit = println(quiet + unneeded)

quiet produces no deprecation warning with -deprecation. unneeded suppresses nothing, which the compiler only says when asked with -Wunused:nowarn:

$ scala compile . -deprecation -Wunused:nowarn
-- Warning: d/Nowarn.scala:12:0
12 |@nowarn
   |^^^^^^^
   |@nowarn annotation does not suppress any warnings
1 warning found

A bare @nowarn with no filter silences everything on the annotated definition, so always give it a filter.

A source-based rule keeps generated code out of the policy: -Wconf:src=src_managed/.*:s. On the command line, quote a -Wconf value that contains * or & so that the shell does not expand it.

A policy for Scala CLI and sbt

A strict file-level policy in Scala CLI, which is the one to start with, is two directives:

//> using scala 3.10.0
//> using options -deprecation -feature -Wunused:all -Werror

package trailhead

import scala.util.Random

@main def strict(): Unit =
  val unused = 1
  println("trailhead")
$ scala compile .
-- [E198] Unused Symbol Warning: r/Strict.scala:6:18
6 |import scala.util.Random
  |                  ^^^^^^
  |                  unused import
-- [E198] Unused Symbol Warning: r/Strict.scala:9:6
9 |  val unused = 1
  |      ^^^^^^
  |      unused local definition
No warnings can be incurred under -Werror
2 warnings found
1 error found
Compilation failed

The equivalent for sbt is the following (not run here, because sbt was not used for this page; the option names are exactly the ones the Scala CLI run above exercised):

scalacOptions ++= Seq(
  "-deprecation",
  "-feature",
  "-Wunused:all",
  "-Wvalue-discard",
  "-Wnonunit-statement",
  "-Wsafe-init"
)

// fail on warnings in CI only
scalacOptions ++= (if (sys.env.contains("CI")) Seq("-Werror") else Nil)

Reading errors: -explain and the error codes

Every compiler error and many warnings carry a code, E and three digits, in the heading line. The heading also names the kind of problem, and the line and column point at the code.

//> using scala 3.10.0
package trailhead

enum Surface:
  case Dirt, Gravel, Paved

def grade(s: Surface): Int = s match
  case Surface.Dirt   => 1
  case Surface.Gravel => 2

@main def err(): Unit =
  val n: Int = "ridge"
  println(grade(Surface.Paved) + n)
$ scala compile .
-- [E007] Type Mismatch Error: f/Err.scala:12:15
12 |  val n: Int = "ridge"
   |               ^^^^^^^
   |               Found:    ("ridge" : String)
   |               Required: Int
   |
   | longer explanation available when compiling with `-explain`
1 error found
Compilation failed

The hint at the bottom is the way into the explanation. -explain appends the compiler’s own reasoning:

$ scala compile . -explain
-- [E007] Type Mismatch Error: f/Err.scala:12:15
12 |  val n: Int = "ridge"
   |               ^^^^^^^
   |               Found:    ("ridge" : String)
   |               Required: Int
   |----------------------------------------------------------------------------
   | Explanation (enabled by `-explain`)
   |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   |
   | Tree:
   |
   | "ridge"
   |
   | I tried to show that
   |   ("ridge" : String)
   | conforms to
   |   Int
   | but none of the attempts shown below succeeded:
   |
   |   ==> ("ridge" : String)  <:  Int
   |     ==> String  <:  Int  = false
   |
   | The tests were made under the empty constraint
    ----------------------------------------------------------------------------
1 error found
Compilation failed

The <: lines are the subtype tests the compiler performed, and the line that ends in = false is the one that failed. -explain is useful when the error is about implicits, variance or a match type, and noisy otherwise; -explain-cyclic does the same for cyclic reference errors only. (-explain-types is deprecated and means -explain.)

The non-exhaustive match in the same file is only reported once the type error is fixed, because the compiler stops after the typer phase when it finds an error. Warnings come with explanations too. With the type error removed:

$ scala compile . -explain
-- [E029] Pattern Match Exhaustivity Warning: g/Exh.scala:7:29
7 |def grade(s: Surface): Int = s match
  |                             ^
  |                             match may not be exhaustive.
  |
  |                             It would fail on pattern case: Paved
  |-----------------------------------------------------------------------------
  | Explanation (enabled by `-explain`)
  |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  | There are several ways to make the match exhaustive:
  |  - Add missing cases as shown in the warning
  |  - If an extractor always return Some(...), write Some[X] for its return type
  |  - Add a case _ => ... at the end to match all remaining cases
   -----------------------------------------------------------------------------
1 warning found

Exhaustivity is a warning, not an error. -Werror is what turns a forgotten enum case into a build failure, which is the usual reason to use it in a project that models its domain with enums and sealed traits (see Enums and Pattern Matching).

Looking up a code

The error codes are stable identifiers, numbered E001 to E234 in Scala 3.10, each with a page in the reference documentation named after the code. A code never changes meaning, so it is what to search for in issue trackers and what to put in a -Wconf:id=... filter. The reference page is https://docs.scala-lang.org/scala3/reference/error-codes/E007.html and the full table is at the index of the same directory. Titles on those pages are the internal names (E198 is "Unused Symbol", E209 is "Format Interpolation Error"), which can differ from the wording of the message you see.

Code Name Example trigger

E003

Deprecated With Operator

A with B as a type

E007

Type Mismatch

val n: Int = "ridge"

E008

Not A Member

trails.lenght on a List[String] (the message adds "did you mean trails.length?")

E018

Illegal Start Simple Expr

collect(case ...) without -preview

E029

Pattern Match Exhaustivity

the Surface match

E030

Match Case Unreachable

a case that can never match

E092

Unchecked Type Pattern

case xs: List[String]

E175 / E176

Value Discarding, Unused Non-Unit Value

-Wvalue-discard, -Wnonunit-statement

E198

Unused Symbol

every -Wunused message

E209

Format Interpolation Error

-Wtostring-interpolated

E221

Recurse With Default

-Wrecurse-with-default

E225

Infer Union Warning

-Winfer-union

E227

Private Shadows Type

-Wshadow

Language versions: -source, -rewrite and -future

-source makes the compiler behave as an earlier or later language version. The choices of 3.10 are every 3.0 to 3.11, each with a -migration variant, and future, with future-migration. A -migration source accepts the old form but warns and says what to change; adding -rewrite makes the compiler change the file.

//> using scala 3.10.0
package trailhead

import scala.collection.mutable._
import scala.language.implicitConversions

object Legacy:
  implicit def intToStr(i: Int): String = i.toString
  def total(xs: Int*): Int = xs.sum
  def greet(implicit n: Int): String = s"hi $n"

@main def migrate(): Unit =
  import Legacy.intToStr
  val buf = ListBuffer(1, 2, 3)
  implicit val n: Int = 7
  println(Legacy.total(buf.toSeq: _*))
  val s: String = 5
  println(Legacy.greet + s)

On 3.10 this compiles (the x: _* form is reported as a warning) and prints 6 and hi 75. Under -source:future the constructs that the language is going to drop become errors, each with a pointer to its migration:

$ scala compile . -source:future
-- Error: h/w/Legacy.scala:4:32
4 |import scala.collection.mutable._
  |                                ^
  |`_` is no longer supported for a wildcard import; use `*` instead
  |This construct can be rewritten automatically under -rewrite -source future-migration.
-- Error: h/w/Legacy.scala:10:12
10 |  def greet(implicit n: Int): String = s"hi $n"
   |            ^
   |`implicit` parameters are no longer supported, use a `using` clause instead
   |This construct can be rewritten automatically under -rewrite -source future-migration.
-- Error: h/w/Legacy.scala:16:34
16 |  println(Legacy.total(buf.toSeq: _*))
   |                                  ^
   |The syntax `x: _*` is no longer supported for vararg splices; use `x*` instead
   |This construct can be rewritten automatically under -rewrite -source 3.4-migration.
-- Error: h/w/Legacy.scala:8:15
8 |  implicit def intToStr(i: Int): String = i.toString
  |               ^
  |`implicit` conversion methods are no longer supported. They can usually be
  |replaced by given instances of class `Conversion`. Example:
  |
  |    given Conversion[Int, String] = i => ...
  |
-- Error: h/w/Legacy.scala:15:15
15 |  implicit val n: Int = 7
   |               ^
   |`implicit` vals are no longer supported, use a `given` clause instead. Example:
   |
   |   given n: Int = ...
   |
   |Note: given clauses are evaluated lazily unless the right hand side is
   |a simple reference.  If eager evaluation of the value's right hand side
   |is important, you can define a regular val and a given instance like this:
   |
   |    val n = ...
   |    given Int = n
   |
5 errors found
Compilation failed

The three messages that say "can be rewritten automatically" are the ones the compiler can fix for you:

$ scala compile . -source:future-migration -rewrite
[patched file h/w/Legacy.scala]

After the rewrite the wildcard import is import scala.collection.mutable.*, the parameter list is def greet(using n: Int) and the vararg splice is buf.toSeq*. The program still prints 6 and hi 75. The two remaining errors, the implicit def conversion and the implicit val, have no automatic rewrite and are fixed by hand. A rewrite changes the files in place: commit first and review the diff like any other change.

A second sample shows the rewrite of the constructs that 3.4 started to warn about, which on 3.10 includes one that is now an error. The file has Named with Sized as a type, List[_], private[this] and xs: _*:

$ scala compile .
-- [E003] Syntax Error: s/w/With.scala:9:18
9 |type Both = Named with Sized
  |                  ^^^^
  |with as a type operator has been deprecated; use & instead
  |This construct can be rewritten automatically under -rewrite -source 3.4-migration.
  |
  | longer explanation available when compiling with `-explain`
-- Warning: s/w/With.scala:11:19
11 |def count(xs: List[_]): Int = xs.size
   |                   ^
   |`_` is deprecated for wildcard arguments of types: use `?` instead
   |This construct can be rewritten automatically under -rewrite -source 3.4-migration.
-- Warning: s/w/With.scala:14:9
14 |  private[this] var n = 0
   |         ^^^^^^
   |Ignoring [this] qualifier.
   |The syntax `private[this]` will be deprecated in the future; just write `private` instead.
   |See: https://docs.scala-lang.org/scala3/reference/dropped-features/this-qualifier.html
   |This construct can be rewritten automatically under -rewrite -source 3.4-migration.
-- Warning: s/w/With.scala:23:20
23 |  println(total(xs: _*) + count(xs) + Counter().inc())
   |                    ^
   |The syntax `x: _*` is no longer supported for vararg splices; use `x*` instead
   |This construct can be rewritten automatically under -rewrite -source 3.4-migration.
3 warnings found
1 error found
Compilation failed

$ scala compile . -source:3.4-migration -rewrite
...
[patched file s/w/With.scala]
4 warnings found

(The second command prints the same four diagnostics as warnings, and A with B is no longer an error under a migration source, and then patches the file.) The diff it produced:

9c9
< type Both = Named with Sized
---
> type Both = Named & Sized
11c11
< def count(xs: List[_]): Int = xs.size
---
> def count(xs: List[?]): Int = xs.size
14c14
<   private[this] var n = 0
---
>   private var n = 0
23c23
<   println(total(xs: _*) + count(xs) + Counter().inc())
---
>   println(total(xs*) + count(xs) + Counter().inc())

After the rewrite the file compiles with no output on 3.10 and the program prints 10.

The built-in rewrites are cumulative per flag: a later flag does not replay an earlier one, so when crossing several minor releases you run -source:3.4-migration -rewrite, then -source:3.6-migration -rewrite and so on in order. The 3.9 release notes tabulate what each flag changes.

Flag (with -rewrite) Rewrites

-source:3.4-migration

with to & in types, _ type wildcards to ?, xs: _* to xs*, var x = _ to scala.compiletime.uninitialized, drop [this] from private[this], drop a trailing _ used for eta-expansion, backtick alphanumeric infix operators that are not declared infix, insert using on explicit arguments to context-bound parameters, insert case on refutable for generators

-source:3.5-migration

no new rewrites

-source:3.6-migration

names on arguments to Java-defined annotations; (x = e) to {x = e} where it would now be a one-element named tuple

-source:3.7-migration

insert using at call sites of implicit parameters, drop an empty () that only existed to pass implicits, rewrite infix named-argument lists to the dotted form

-source:3.8-migration

refutable pattern bindings gain .runtimeChecked, : @unchecked ascriptions become .runtimeChecked

-source:3.9-migration

backtick identifiers that contain $

-source:3.3 on a newer compiler goes the other way: it tells the compiler to treat the sources as 3.3 and to stop reporting the later warnings, a short-term escape hatch for code that cannot be updated yet. The step-by-step procedure for a real upgrade is in Upgrading Between Scala 3 Versions, the Scala 2 route in Migrating from Scala 2, and the list of removed features in Incompatibilities & Dropped Features.

The syntax-style options -new-syntax (require then and do), -old-syntax (require parentheses around conditions) and -no-indent (classic braces only), together with -rewrite -indent, convert a code base between the braced and the braceless style (see Optional Braces & Indentation).

Preview and experimental

Unfinished language features sit behind two flags, which differ in their promise.

  • -preview (3.7) enables preview features anywhere in the project. A preview feature is on its way to the stable language; it can still change incompatibly before then. In 3.10 the previews are @unroll, relaxed lambda syntax and referenceable package objects, plus partial function literals with parentheses (SIP-74).

  • -experimental marks every top-level definition of the project @experimental, which allows the use of experimental definitions anywhere in it. An experimental feature may be changed or dropped at any time. The finer-grained route is a single import scala.language.experimental.<feature> or @experimental on one definition.

The SIP-74 form, one of the 3.10 previews, shows the difference between the two states. The program is:

//> using scala 3.10.0
package trailhead

@main def prev(): Unit =
  val xs = List((1, 1), (2, 3), (3, 5))
  println(xs.collect(case (a, b) if b > 4 => a))
$ scala compile .
-- [E018] Syntax Error: i/Prev.scala:6:21
6 |  println(xs.collect(case (a, b) if b > 4 => a))
  |                     ^^^^
  |                     expression expected but case found
  |
  | longer explanation available when compiling with `-explain`
1 error found
Compilation failed

$ scala run . -preview
List(3)

An experimental definition is rejected in a normal project, with the three ways out named in the message:

//> using scala 3.10.0
package trailhead

import scala.annotation.experimental

@experimental def shiny(): Int = 1

@main def exp(): Unit = println(shiny())
$ scala compile .
-- Error: j/Exp.scala:8:32
8 |@main def exp(): Unit = println(shiny())
  |                                ^^^^^
  |         method shiny is marked @experimental
  |
  |         Experimental definition may only be used under experimental mode:
  |           1. in a definition marked as @experimental, or
  |           2. an experimental feature is imported at the package level, or
  |           3. compiling with the -experimental compiler flag.
1 error found

scala compile . -experimental accepts the same file. The experimental flag is contagious in the way that matters to library authors: the code compiled with it is itself marked experimental, so other projects cannot use it without opting in. The 3.10 release notes deprecated the old experimental language imports of the three SIPs that moved to preview; using them prints a warning that points to -preview.

-language:<feature> is the third, older mechanism. It enables ordinary language features that must be switched on (implicitConversions, dynamics, postfixOps, strictEquality, adhocExtensions, unsafeNulls, noAutoTupling) and the experimental imports by name, and -language:help prints the list for the compiler in use. The state of each preview and experimental feature is tracked in Language Evolution: SIPs, Preview & Experimental.

The recursion limit: -Xmax-fuel (3.10)

Before 3.10 the compiler tried to catch a StackOverflowError while type checking deeply recursive types, which was not reliable. From 3.10 the recursive operations of the compiler consume fuel, an abstract budget, and when the fuel is gone compilation stops with "Recursion limit exceeded" instead of crashing. The default is 550 (-Xmax-fuel:help prints it), chosen to run out before the stack of a normally configured JVM does.

//> using scala 3.10.0
package trailhead

import scala.compiletime.ops.int.*

type Down[N <: Int] <: Int = N match
  case 0 => 0
  case _ => Down[N - 1]

@main def fuel(): Unit =
  val z: Down[400] = 0
  println(z)

A match type that reduces 400 levels deep is intended code, and it exhausts the default budget. This is the error (shortened in the middle; the real message lists the recurring operation with its argument for each level):

$ scala compile .
-- Error: k/Fuel.scala:11:9
11 |  val z: Down[400] = 0
   |         ^^^^^^^^^
   |Recursion limit exceeded.
   |Maybe there is an illegal cyclic reference?
   |If that's not the case, you could try to increase the fuel and stack size: https://docs.scala-lang.org/overviews/compiler-options/compiling-deeply-nested-code.html
   |For the unprocessed stack overflow trace, compile with -Xno-enrich-error-messages.
   |A recurring operation is (inner to outer):
   |
   |  match cases for (220 : Int)
   |  match cases for (221 : Int)
   |  match cases for (222 : Int)
   |  ...
   |
   |  match cases for (398 : Int)
   |  match cases for (399 : Int)
   |  match cases for (400 : Int)
Compilation failed

Raising the budget makes it compile, and the program prints 0:

$ scala run . -Xmax-fuel:100000
0

Raising -Xmax-fuel without also enlarging the compiler’s stack just turns the error back into a real StackOverflowError. The stack is a property of the JVM that runs the compiler, so it is set with -Xss (for example -Xss16M): in sbt put it in .jvmopts, and with Scala CLI use //> using javaOpt -Xss16M. A StackOverflowError on 3.10 with the default fuel and the default stack is a compiler bug and should be reported. The same program compiled with the 3.9.0 compiler succeeded without any option, because the older compiler had no fuel counter; that is the one place where a build can regress when the compiler is upgraded, and the first thing to check before assuming that the match type is wrong. -Xmax-fuel is separate from -Xmax-inlines (default 32), which limits successive inline expansions; deep recursive inlines spend fuel as well, so -Xmax-inlines alone may not be enough (see Match Types and Inline).

The optimizer: -opt (3.8.3)

The JVM backend gained the Scala 2 bytecode optimizer in 3.8.3. It is opt-in. -opt enables the local optimizations, and -opt-inline:<patterns> tells the inliner where code may be inlined from. A pattern is a class or package glob (a.C, a.*, a.**), <sources> stands for the classes compiled in the current run, and a leading ! excludes. The standard application setting from the documentation is -opt -opt-inline:**,!java.**, which allows inlining of everything but the JDK.

The optimizer is meant for an application build at release time and not for a published library: inlining copies code from one class into another, which breaks binary compatibility between releases, ties the build to the exact dependency versions of the compile-time class path, and defeats incremental compilation. Methods marked @inline are only a hint, and -Wopt reports the ones the optimizer could not honour:

//> using scala 3.10.0
package trailhead

class Gauge:
  @inline def reading(x: Int): Int = x * 2

object Hot:
  @inline def square(x: Int): Int = x * x

def total(g: Gauge): Int =
  var sum = 0
  for i <- 1 to 10 do sum += Hot.square(i) + g.reading(i)
  sum

@main def opt(): Unit = println(total(Gauge()))
$ scala compile . -opt '-opt-inline:<sources>' -Wopt:at-inline-failed
-- Warning: l/Opt.scala:12:54
12 |  for i <- 1 to 10 do sum += Hot.square(i) + g.reading(i)
   |                                             ^^^^^^^^^^^^
   |trailhead/Gauge::reading(I)I is annotated @inline but could not be inlined:
   |The method is not final and may be overridden.
1 warning found

$ scala run . -opt '-opt-inline:<sources>'
495

Hot.square is a method of an object and gets no warning. Gauge.reading belongs to a non-final class, can be overridden, and so cannot be resolved at compile time. Making the method or the class final is the fix. The optimizer only changes method bodies, never signatures; inlined methods disappear from stack traces, and line numbers are lost when a method is inlined into a different class file. Quote <sources> and the * patterns on the command line, because the shell treats <, > and * specially. Scala 3’s compile-time inline (see Inline) is a different mechanism and does not need this flag.

Comparison with Java, Kotlin and Scala 2

Concern Scala 3 Java (javac) Kotlin

Deprecation and unchecked details

-deprecation, -feature, -unchecked

-Xlint:deprecation, -Xlint:unchecked

the compiler reports deprecations by default

Unused code

-Wunused:all

no compiler check (IDE inspection or an analyser)

IDE inspections; compiler reports unused variables

Warnings as errors

-Werror (also -Xfatal-warnings), tuned by -Wconf

-Werror, tuned by -Xlint: lists and @SuppressWarnings

allWarningsAsErrors, tuned by @Suppress

Suppress at one place

@nowarn("cat=...")

@SuppressWarnings("...")

@Suppress("...")

Error identifiers

E001 to E234, -explain

none (the message text only)

none

Language version

-source:3.x, -source:future, -rewrite

--release, -source

-language-version, -api-version

Unfinished features

-preview, -experimental

--enable-preview

-opt-in, experimental annotations

Scala 2 users have to learn new names. -Xlint and -Ywarn-* are not recognised by the Scala 3 compiler (they are ignored with a bad option warning, as shown above), and -Xfatal-warnings still works as an alias. Flag sets generated by a build plugin must be a version that targets Scala 3. Most of what Scala 2 needed -Xlint for is now in the -W family; Scalafix (third-party) covers the rules the compiler does not.

Pitfalls

  • Unknown options are ignored with a warning, not rejected. A typo such as -Wunsed:all produces bad option '-Wunsed:all' was ignored, a warning that disappears among the others. Under -Werror it becomes a build failure, which is the quickest way to find the typo.

  • -Werror in a published library’s build makes every JDK or dependency upgrade a possible build failure. Scope it to CI, or keep it and silence specific ids with -Wconf.

  • -Wconf order matters. The last matching rule wins, so a broad any:e must come before the specific silences.

  • -Wunused:all and generated code: silence the generated directory with a src= rule instead of dropping the lint.

  • -rewrite edits sources in place. Run it on a clean working tree and review the diff.

  • -Xmax-fuel without -Xss converts a clean error into a stack overflow.

  • -opt in a library breaks binary compatibility; use it only for the application artifact.

  • -experimental hides what is experimental. Prefer the single import scala.language.experimental.x so that the dependency on an unstable feature stays visible in the file that uses it.

  • -source:future is a probe, not a target. The set of constructs that it rejects grows with every release.

Where the books are outdated

Material written for Scala 2 and for the first Scala 3 releases lists -Xlint, -Ywarn-unused and -Xfatal-warnings as the lint toolkit. In Scala 3.10 the lint options are the -W family, -Werror replaces -Xfatal-warnings, and -Wunused has been rewritten with the choices shown above. Earlier text about -explain-types points to -explain. Anything written before 2023 predates -preview (3.7), -opt (3.8.3), -Xmax-fuel (3.10), the -source:3.x-migration -rewrite sequence documented for 3.9, -Wnonunit-statement, -Wsafe-init and the origin= filter of -Wconf; for error codes, look the code up in the reference pages instead of relying on a printed list.