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 |
|
Scala CLI, in the source file |
a |
|
sbt |
|
|
The compiler launcher from the distribution |
plain arguments |
|
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 |
|---|---|
|
standard options: |
|
warnings: |
|
advanced settings that are supported: |
|
verbose and debugging output |
|
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 |
|---|---|---|
|
an import selector that is never referenced |
|
|
a private field or method that is never used |
|
|
a local |
|
|
an explicit parameter that the body ignores |
|
|
a |
|
|
both of the two above |
|
|
a name bound in a pattern and never used |
|
|
|
|
|
every choice, including |
|
|
a |
|
//> 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-discardwarns when a non-Unitvalue is converted toUnitonly because the expected type isUnit, the waydef f(): Unit = g()silently drops `g()’s result. -
-Wnonunit-statementwarns when a statement in a block has a non-Unittype 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 |
|---|---|
|
a constructor parameter that shadows an inherited field ( |
|
a standard interpolator that calls |
|
a method that calls itself and relies on a default argument |
|
a type argument that the compiler inferred as a union, which often hides a mistake such as
|
|
a pattern whose comparison looks like it can never succeed |
|
|
|
the safe initialization check (see below) |
|
optimizer diagnostics, see The optimizer: -opt (3.8.3) |
|
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 |
|---|---|
|
every message |
|
a message category |
|
part of the message text |
|
the message id, which is printed in the heading of each warning |
|
the message name, shown in verbose mode |
|
the full path of the source file |
|
the full name ( |
| Action | Effect |
|---|---|
|
report as an error |
|
report as a warning |
|
report as a warning and print the filters that matched |
|
report as information; not counted by |
|
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
Source: Scala 3 Reference: Error codes.
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 |
|---|---|---|
|
Deprecated With Operator |
|
|
Type Mismatch |
|
|
Not A Member |
|
|
Illegal Start Simple Expr |
|
|
Pattern Match Exhaustivity |
the |
|
Match Case Unreachable |
a case that can never match |
|
Unchecked Type Pattern |
|
|
Value Discarding, Unused Non-Unit Value |
|
|
Unused Symbol |
every |
|
Format Interpolation Error |
|
|
Recurse With Default |
|
|
Infer Union Warning |
|
|
Private Shadows Type |
|
Source: Scala 3 Reference: Error codes.
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 |
|---|---|
|
|
|
no new rewrites |
|
names on arguments to Java-defined annotations; |
|
insert |
|
refutable pattern bindings gain |
|
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). -
-experimentalmarks 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 singleimport scala.language.experimental.<feature>or@experimentalon 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 |
|
|
the compiler reports deprecations by default |
Unused code |
|
no compiler check (IDE inspection or an analyser) |
IDE inspections; compiler reports unused variables |
Warnings as errors |
|
|
|
Suppress at one place |
|
|
|
Error identifiers |
|
none (the message text only) |
none |
Language version |
|
|
|
Unfinished features |
|
|
|
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:allproducesbad option '-Wunsed:all' was ignored, a warning that disappears among the others. Under-Werrorit becomes a build failure, which is the quickest way to find the typo. -
-Werrorin 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. -
-Wconforder matters. The last matching rule wins, so a broadany:emust come before the specific silences. -
-Wunused:alland generated code: silence the generated directory with asrc=rule instead of dropping the lint. -
-rewriteedits sources in place. Run it on a clean working tree and review the diff. -
-Xmax-fuelwithout-Xssconverts a clean error into a stack overflow. -
-optin a library breaks binary compatibility; use it only for the application artifact. -
-experimentalhides what is experimental. Prefer the singleimport scala.language.experimental.xso that the dependency on an unstable feature stays visible in the file that uses it. -
-source:futureis 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.