Incompatibilities & Dropped Features

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 program, diagnostic and rewrite on it was produced with Scala CLI 1.17.1 on JDK 26 on 2026-10-16. Where a result depends on another compiler it was produced with Scala 3.9.0 (the LTS line), 3.3.8 (the previous LTS) or 2.13.18, each fetched from Maven Central through the same Scala CLI. The page answers one question: what stops a piece of Scala 2.13 code, or a pattern remembered from an older book, from compiling or from behaving the same under Scala 3.10, and what replaces it.

The official migration guide groups the answers into an incompatibility table (syntactic changes, dropped features, contextual abstractions, other changed features, type checker, type inference and macros), and the language reference has a separate dropped features chapter. Both are covered row by row below, then merged with the changes that Scala 3 itself made between 3.3 and 3.10, and finally with the outdated forms found in books. Planning a whole migration is the subject of Migrating from Scala 2; which compiler can read whose output is covered in Scala 2 & Scala 3 Compatibility; moving between Scala 3 versions is in Upgrading Between Scala 3 Versions. This page is the reference those pages point to.

The examples use the trailhead scenario (trails, huts and a Planner) where it fits, and a minimal standalone program where it does not. Each failing example shows the exact compiler message from 3.10.0, with the file path removed.

What counts as an incompatibility

An incompatibility is a piece of code that compiles with Scala 2.13 and does not compile with Scala 3. Two refinements matter in practice:

  • A runtime incompatibility is code that compiles on both but behaves differently. They are rare and dangerous, so this page flags every one it knows: implicit views (Implicit views), the inferred type of an overriding method (Return type of an override), a -> defined through an implicit class (-> is now an extension method), and private[this] fields (private[this] and protected[this]).

  • A removal is not always an incompatibility in the strict sense. Scala 3.0 to 3.3 kept some Scala 2 syntax alive for migration, then Scala 3.4 started to warn about it and Scala 3.10 turned some of those warnings into errors, so code that was fine on the 3.3 LTS can stop compiling on 3.10 even though it is Scala 3 code. The timeline is in From warnings to errors: the 3.3 to 3.10 timeline.

Three outcomes appear in the tables below. Error means the compiler rejects the code on 3.10. Warning means it compiles and the message names the replacement. Silent means it compiles with no message at the default settings.

At a glance

Incompatibility Scala 3.10 result Auto-rewrite Replacement

Syntactic changes

Restricted keywords (given, enum, export, then …​)

error

migration mode

backticks or another name

Procedure syntax

error

migration mode, Scalafix

: Unit =

Lambda parameter without parentheses

error

migration mode

(x: Int) => ...

Open brace on its own line

error

migration mode

brace on the line of the call

Wrong indentation

error

none

fix the indentation

_ as a type parameter

error

none

a real name such as T

+ and - as type parameters

error

none

a real name such as T

Dropped features

Symbol literals

error

migration mode

Symbol("abc") or a string

do-while

error

migration mode

while with a block condition

Auto-application

error

migration mode, Scalafix

write ()

Value eta-expansion x _

error

migration mode, Scalafix

() => x

any2stringadd

error

Scalafix

String.valueOf(x) + ...

Early initializers

error

none

trait parameters

Existential types

error

none

? wildcards or type members

@specialized

silent, ignored

none

inline

Contextual abstractions

Type of an implicit definition

error

Scalafix

write the type

Implicit views

error, may change behavior

none

call the conversion

View bounds

error

none

implicit parameter or using

Conversion on A and => A

error

none

call the conversion

Other changed features

Inheritance shadowing

error

migration mode

this.x

Non-private constructor in a private class

error

warning only

make it private

Abstract override

error

none

remove the abstract def

Case class companion as a function

error

none

Foo.apply as a value

Explicit call to unapply

error

none

pattern binding

Invisible bean property

error

none

pojo.fooBar

=> T as a type argument

error

none

own method or inlining

Wildcard on a higher-kinded member

error

none

type parameter or wrapper

Type checker

Variance checks

error

none

invariance, bounds, overload

Pattern matching soundness

error

none

relax the types

Type inference

Return type of an override

inferred type changes

none

write the type

Reflective type

error on use

none

write the structural type

Macros

Scala 2 macros

error

none

inline, quotes and splices

Letting the compiler do the work first

Most syntactic and dropped-feature rows have an automatic rewrite. It lives in the compiler’s migration mode: -source:3.0-migration turns the hard errors of the rows above into migration warnings and, together with -rewrite, patches the files in place. Run it on a clean working tree so that git diff shows exactly what changed.

The source file below packs eight rows of the table into Scala 2 style code. It is deliberately brace-based, the style the rewrite expects.

//> using scala 3.10.0

object given {
  val enum = 1
}

class Trail {
  val km = 2
}

object Walk {
  val km = 9
  class Day extends Trail {
    def show(): Unit = println(km)
  }

  def print() { println("walk") }

  val labels = Map('easy -> 1)
  val square = { x: Int => x * x }
  val ascent = 40
  val lazyAscent: () => Int = ascent _

  def message(): String = "hello"

  def run(): Unit = {
    var i = 0
    do {
      i += 1
    } while (i < 3)
    println(message)
    println(i)
  }
}

Compiling it with the default settings fails with eight errors, one per row (the messages are shown in the sections below). Compiling it with scala compile --scalac-option -rewrite --scalac-option -source:3.0-migration Legacy.scala prints a single migration warning for the auto-application and the line [patched file Legacy.scala]. This is the diff the rewrite produced:

-object given {
-  val enum = 1
+object `given` {
+  val `enum` = 1
...
-    def show(): Unit = println(km)
+    def show(): Unit = println(this.km)
...
-  def print() { println("walk") }
+  def print(): Unit = { println("walk") }

-  val labels = Map('easy -> 1)
-  val square = { x: Int => x * x }
+  val labels = Map(Symbol("easy") -> 1)
+  val square = { (x: Int) => x * x }
...
-  val lazyAscent: () => Int = ascent _
+  val lazyAscent: () => Int = (() => ascent)
...
-    do {
+    while ({ {
...
-    } while (i < 3)
-    println(message)
+    } ; i < 3}) ()
+    println(message())

The patched file compiles on 3.10 with exit code 0. The rewritten code is correct but not pretty (while ({ { ... } ; c }) () is the literal desugaring of do), so review it as ordinary source.

Two remarks about the rewrite. First, it only repairs rows marked "migration mode" in the table above; the other rows need a human. Second, a rewrite run does not replay earlier ones: -source:3.4-migration patches only what became deprecated in 3.4 (see From warnings to errors: the 3.3 to 3.10 timeline), not the 3.0 items. The Scala 2.13 compiler can find the same constructs before migration starts when run with -Xsource:3, and Scalafix has rules for the rows marked "Scalafix" (procedure syntax, auto-application, eta-expansion, any2stringadd, explicit implicit types). Scalafix is a third-party tool from the Scala Center; it is not part of the Scala distribution. See Migrating from Scala 2 for the full workflow.

The same code on 2.13.18

To make the "compiles on 2.13" claim concrete, the next program mixes procedure syntax, a symbol literal, a view bound, an early initializer, an implicit definition without a type, auto-application, any2stringadd, a bean property, the curried companion of a case class and a do loop. It was run with //> using scala 2.13.18 and //> using options -deprecation -feature.

warning: procedure syntax is deprecated: instead, add `: Unit =` to explicitly declare `proc`'s return type [quickfixable]
warning: symbol literal is deprecated; use Symbol("abc") instead [quickfixable]
warning: view bounds are deprecated; use an implicit parameter instead.
warning: early initializers are deprecated; they will be replaced by trait parameters in 3.0
warning: Implicit definition should have explicit type (inferred Int) [quickfixable]
warning: Auto-application to `()` is deprecated. Supply the empty argument list `()` explicitly to invoke method message
warning: method any2stringadd in object Predef is deprecated (since 2.13.0): Implicit injection of + is deprecated.
7 warnings
procedure syntax
hello
3
16
3
Symbol(abc)
Foo(1,true)
x
true
1
3

Seven warnings and a successful run. Every warning points to the replacement that Scala 3 requires, so a 2.13 build with -deprecation (and -Xsource:3) is the cheapest preparation for the move. Not every row has a 2.13 warning: the do loop, the lambda without parentheses, the bean property and the case-class companion functions compiled silently in this program and fail on 3.

Syntactic changes

Scala 3 introduced optional braces and the new control syntax. That costs a few restrictions on the older syntax and removes some oddities. The migration guide lists seven rows.

Restricted keywords

enum, export, given, then, =>> and ?=> are regular keywords in Scala 3 and cannot be identifiers any more (soft keywords such as inline, opaque, extension, using, open, transparent and infix are unaffected). The migration guide’s example is object given { val enum = ... }:

//> using scala 3.10.0

object given:
  val enum = 1
  println(enum)
-- [E040] Syntax Error: kw.scala:3:7
3 |object given:
  |       ^^^^^
  |       an identifier expected, but 'given' found
-- [E103] Syntax Error: kw.scala:5:2
5 |  println(enum)
  |  ^^^^^^^
  |  Illegal start of toplevel definition
2 errors found

The fix is a different name or backticks around the name, and the migration rewrite chooses backticks (see the diff above, where both given and enum are quoted). A Java method with such a name, for example then, is called with the same backticks, but the practical remedy is to rename on your side.

Procedure syntax

def print() { ... }, with no =, declared a method returning Unit in Scala 2. It was deprecated in 2.13 and is dropped in Scala 3.

//> using scala 3.10.0

object Bar:
  def print() {
    println("bar")
  }
-- [E040] Syntax Error: proc.scala:4:14
4 |  def print() {
  |              ^
  |              '=' expected, but '{' found
1 error found

Write def print(): Unit = .... The rewrite inserts : Unit = (see the diff above). Note that the form made a method return Unit whatever its body computed.

Parentheses around a lambda parameter

A lambda parameter followed by a type must be parenthesized: { x: Int => x * x } is ambiguous with the optional-braces syntax and is rejected.

-- Error: lambda.scala:3:17
3 |val f = { x: Int => x * x }
  |                 ^
  |parentheses are required around the parameter of a lambda
  |This construct can be rewritten automatically under -rewrite -source 3.0-migration.
1 error found

The fix is { (x: Int) => x * x }, which also compiles on 2.13, so the change can be made before the migration. The message itself tells you the rewrite switch.

Open brace indentation for passing an argument

A Scala 2 style that puts the argument block on the next line, after the call, is no longer parsed as an argument.

//> using scala 3.10.0

def test(name: String)(body: => Unit): Unit = body

@main def braceDemo(): Unit =
  test("my test")
  {
    assert(1 == 1)
  }

On 3.10 the brace is a new statement, and the call before it lacks its second argument list:

-- [E178] Type Error: brace.scala:6:2
6 |  test("my test")
  |  ^^^^^^^^^^^^^^^
  |  missing argument list for value of type (=> Unit) => Unit
1 error found

The migration rewrite indents the brace so that it belongs to the call; the cleaner fix is to move it up: test("my test") {. The same rewrite handles a refinement placed after a new line (type Bar = Foo followed by {).

Wrong indentation

Scala 3 uses indentation to find the end of a statement more strictly. In the guide’s example an indented line after a value is read as an application of that value, not a new statement:

//> using scala 3.10.0

def bar: (Int, Int) = {
  val foo = 1.0
  val bar = foo
    (1, 1)
}
-- [E050] Type Error: indent.scala:5:12
5 |  val bar = foo
  |            ^^^
  |            value foo does not take parameters
6 |    (1, 1)
-- [E007] Type Mismatch Error: indent.scala:7:1
7 |}
  | ^
  |Found:    Unit
  |Required: (Int, Int)
  |
  |Maybe the enclosing block is missing a final expression after the definition of `bar`?
2 errors found

There is no automatic rewrite; fix the indentation so that (1, 1) lines up with val. A formatter such as scalafmt prevents the mistake, but it may restyle the whole project, so adopt it as a separate commit. The rest of the indentation rules are in Optional Braces & Indentation.

_ as a type parameter

Scala 2 accepted def foo[_: Foo], a type parameter named _ together with a context bound, a trick the fastparse library used. Martin Odersky called it a "clever exploit of a scalac compiler bug", and Scala 3 closes it.

-- [E040] Syntax Error: under.scala:4:8
4 |def foo[_: Foo]: Unit = ()
  |        ^
  |        an identifier expected, but '_' found
1 error found

Give the parameter a real name (def foo[T: Foo]). Binary compatibility is unaffected because type parameters are erased.

+ and - as type parameters

def foo[+] named a type parameter + in Scala 2. Scala 3 reads the sign as a variance annotation and finds no name.

-- Error: plusminus.scala:3:8
3 |def foo[+]: Unit = ()
  |        ^
  |        no `+/-` variance annotation allowed here
1 error found

Choose an ordinary name. + and - remain valid as type names in general (type +), only the parameter position changed.

Dropped features

The second group of rows removes features "to simplify the language". Eight rows, all of which fail on 3.10.

Symbol literals

'abc was a literal of type Symbol in Scala 2. The syntax is deprecated in 2.13 and gone in Scala 3, where ' begins a quote for metaprogramming. The class scala.Symbol itself still exists.

//> using scala 3.10.0

val values: Map[Symbol, Int] = Map('abc -> 1)
val abc = values('abc)
-- Error: sym.scala:3:35
3 |val values: Map[Symbol, Int] = Map('abc -> 1)
  |                                   ^
  |symbol literal 'abc is no longer supported,
  |use a string literal "abc" or an application Symbol("abc") instead,
  |or enclose in braces '{abc} if you want a quoted expression.
  |For now, you can also `import language.deprecated.symbolLiterals` to accept
  |the idiom, but this possibility might no longer be available in the future.
-- Error: sym.scala:4:17
4 |val abc = values('abc)
  |                 ^
  |symbol literal 'abc is no longer supported,
  ...
2 errors found

The rewrite produces Symbol("abc"). The migration guide recommends a second step: replace Symbol with plain strings or a dedicated class, because Symbol itself is deprecated. The message names the escape hatch import language.deprecated.symbolLiterals, which keeps old files compiling while they are being converted.

do-while

do now introduces the body of for, while and if in the new control syntax, so do { ... } while (cond) could not stay. The program below shows what 3.10 says:

//> using scala 3.10.0

@main def doWhileDemo(): Unit =
  var i = 0
  do {
    i += 1
  } while (i < 3)
  println(i)
-- Error: dowhile.scala:5:2
5 |  do {
  |  ^^
  |  end of toplevel definition expected but 'do' found
-- [E103] Syntax Error: dowhile.scala:8:2
8 |  println(i)
  |  ^^^^^^^
  |  Illegal start of toplevel definition
2 errors found

The replacements are while ({ body; cond }) (), which also compiles on Scala 2 and is what the rewrite emits, or the Scala 3 form below. The loop-and-a-half style, a while whose condition block runs the body first, is the idiom:

//> using scala 3.10.0

@main def whileDemo(): Unit =
  var i = 0
  while
    i += 1
    i < 3
  do ()
  println(i)

which prints 3. For a loop that walks a collection, prefer for or a higher-order function; see Conditionals & Loops.

Auto-application

A method declared with an empty parameter list, def message(): String, could be called as message in Scala 2. Scala 3 requires the parentheses; Java-defined methods are exempt, because their signatures cannot express the difference.

//> using scala 3.10.0

object Hello:
  def message(): String = "Hello"

@main def autoDemo(): Unit =
  println(Hello.message)
  println("abc".length)
-- [E100] Syntax Error: autoapp.scala:7:16
7 |  println(Hello.message)
  |          ^^^^^^^^^^^^^
  |          method message in object Hello must be called with () argument
1 error found

Only the first call fails: "abc".length calls a Java method and compiles. The rewrite adds the () (println(message()) in the diff above) and Scalafix can do it on 2.13 beforehand. The convention that follows is the one the style guide gives: use () for methods with side effects and no () for pure accessors.

Value eta-expansion

Scala 3 expands a method to a function automatically (automatic eta-expansion), which makes the trailing m _ syntax unnecessary. It also stops turning a plain value into a nullary function:

//> using scala 3.10.0

def twice(n: Int): Int = n * 2

@main def etaDemo(): Unit =
  val x = 1
  val f: () => Int = x _
  val g = twice _
  println((f(), g(4)))
-- [E099] Syntax Error: eta.scala:7:23
7 |  val f: () => Int = x _
  |                     ^^^
  |Only function types can be followed by _ but the current expression has type Int
-- Warning: eta.scala:8:16
8 |  val g = twice _
  |          ^^^^^^^
  |The trailing ` _` for eta-expansion is unnecessary;
  |the compiler performs eta-expansion automatically, so you can write the function value directly instead.
  |This construct can be rewritten automatically under -rewrite -source 3.4-migration.
1 warning found
1 error found

The two lines differ in severity. x _ on a value is an error; the rewrite replaces it with (() => x). twice _ on a method is only a warning since Scala 3.4 (write plain twice). The migration guide lists the row as a Scala 2.13 deprecation, but 2.13.18 itself rejects x _ on an Int with _ must follow method; cannot follow Int, so in practice the row concerns method references.

any2stringadd

Scala 2 had an implicit conversion Predef.any2stringadd that added + to every type, so new AnyRef + "foo" compiled. It is deprecated in 2.13 and removed in Scala 3.

-- [E008] Not Found Error: any2.scala:4:23
4 |  val str = new AnyRef + "foo"
  |            ^^^^^^^^^^^^
  |            value + is not a member of Object
1 error found

The replacement is an explicit conversion, String.valueOf(x) + "foo", or string interpolation (see Strings & Interpolation). The Scalafix rule fix.scala213.Any2StringAdd from scala/scala-rewrites applies it on 2.13.

Early initializers

extends { val name = "Foo" } with Bar initialized fields before the parent trait’s body. They were rarely used and mostly compensated for the lack of trait parameters, which Scala 3 now has.

//> using scala 3.10.0

trait Bar:
  val name: String
  val size: Int = name.size

object Foo extends {
  val name = "Foo"
} with Bar
-- Error: early.scala:7:19
7 |object Foo extends {
  |                   ^
  |                   `extends` must be followed by at least one parent
-- [E009] Syntax Error: early.scala:9:2
9 |} with Bar
  |  ^^^^
  |  Early definitions are not supported; use trait parameters instead
...
3 errors found

The Scala 3 form passes the value as a trait parameter:

//> using scala 3.10.0

trait Bar(val name: String):
  val size: Int = name.size

object Foo extends Bar("Foo")

@main def earlyFix(): Unit =
  println(Foo.size)

prints 3. A trait parameter is not available in 2.13, so code that cross-compiles needs an intermediate class (abstract class BarEarlyInit(val name: String) extends Bar) or a secondary constructor. Early initializers were also used to give a subclass private state that the superclass constructor reads through an overridden method; the migration guide moves that state into a nested private object state, which is initialized on demand. Trait parameters are covered in Traits & Mixins.

Existential types

T forSome { type T } is dropped. The reference gives three reasons: it violates the soundness principle on which DOT (the calculus under Scala 3) is built, it interacts badly with other features, and it overlaps path-dependent types.

-- [E034] Syntax Error: exist.scala:3:24
3 |def foo: List[Class[T]] forSome { type T } = Nil
  |                        ^^^^^^^
  |                        Existential types are no longer supported -
  |                        use a wildcard or dependent type instead
...
3 errors found

Wildcard-only existentials remain legal and are treated as refined types, so List[?] replaces List[T] forSome { type T }. When a wildcard is not enough, wrap the type in an enclosing type that carries it as a member:

trait Bar:
  type T
  val value: List[Class[T]]

When the compiler reads a Scala 2 class file that contains an existential, it approximates it and warns that a precise emulation is not possible. Wildcards are written ? since 3.4; _ still works but warns (see From warnings to errors: the 3.3 to 3.10 timeline).

@specialized

@specialized made the Scala 2 compiler generate primitive-typed copies of a generic class. Scala 3 ignores the annotation (only Function and Tuple have limited specialization) and does not even warn:

//> using scala 3.10.0

class Box[@specialized(Int) A](val a: A)

@main def specDemo(): Unit =
  println(Box(3).a)

prints 3. This is the one "silent" row of the table: the code compiles and runs, but the annotation has no effect, which can matter for hot paths that relied on the specialized variants. Similar benefits can come from inline methods, see Inline.

Contextual abstractions

The redesign of implicits (given, using, Conversion, extension) brought four well-defined incompatibilities, and changed the implicit resolution rules, which can make an unchanged implicit parameter unresolvable. The new rules are in Conversions, into & Implicit Resolution; the new constructs in Givens & Using Clauses.

Type of an implicit definition

Implicit `val`s and `def`s need an explicit result type in Scala 3; the type cannot be inferred from the right-hand side any more.

//> using scala 3.10.0

implicit val answer = 42

@main def implTypeDemo(): Unit =
  println(summon[Int])
-- Error: impltype.scala:3:13
3 |implicit val answer = 42
  |             ^
  |             type of implicit definition needs to be given explicitly
-- [E172] Type Error: impltype.scala:6:21
6 |  println(summon[Int])
  |                     ^
  |No given instance of type Int was found for parameter x of method summon in object Predef
2 errors found

Add the type (implicit val answer: Int = 42), or better, convert it to given answer: Int = 42. The Scalafix rule ExplicitResultTypes writes the missing types on 2.13, and 2.13.18 already warns about the case with Implicit definition should have explicit type (inferred Int).

Implicit views

Scala 3 does not turn an implicit function value parameter ev: A => B into an implicit conversion, so the body below can no longer call print on an A.

//> using scala 3.10.0

trait Pretty:
  val print: String

def pretty[A](a: A)(implicit ev: A => Pretty): String =
  a.print
-- [E008] Not Found Error: views.scala:7:4
7 |  a.print
  |  ^^^^^^^
  |  value print is not a member of A
1 error found

This is the first runtime incompatibility of the page. In the migration guide’s second example, where a broader implicit def anyPretty(any: Any): Pretty is in scope, the code compiles in both languages, but under -source:3.0-migration the compiler applies ev, while under the default -source:3.0 it silently applies anyPretty, which prints any. The fix is to call the conversion explicitly, ev(a).print, or to turn the parameter into a real conversion with using:

//> using scala 3.10.0

trait Pretty:
  val print: String

given Conversion[Int, Pretty] = n => new Pretty { val print = s"<$n>" }

def pretty[A](a: A)(using ev: A => Pretty): String =
  ev(a).print

@main def viewsFixDemo(): Unit =
  println(pretty(7))

The output is <7>. The compiler’s migration mode warns about these sites but does not rewrite them.

View bounds

A <% B was shorthand for "an implicit conversion from A to B exists". It was deprecated for years and is removed.

-- Error: viewbound.scala:3:10
3 |def foo[A <% Long](a: A): Long = a
  |          ^
  | view bounds `<%' are no longer supported, use a context bound `:' instead
-- [E007] Type Mismatch Error: viewbound.scala:3:33
...
2 errors found

The diagnostic suggests a context bound, but that would change the method’s signature. The binary-compatible fix keeps the same erased signature by writing the conversion out as an implicit parameter and calling it:

//> using scala 3.10.0

def foo[A](a: A)(using ev: A => Long): Long = ev(a)

@main def viewBoundFix(): Unit =
  println(foo(5))

which prints 5 (an Int converts to Long by numeric widening). implicit ev is also fine on 2.13 and 3. Beware the runtime incompatibility just described. A conversion that is not meant to be implicit should become an ordinary type class, see Type Classes.

Ambiguous conversion on A and => A

When two implicit conversions exist, one from Boolean and one from => Boolean, Scala 2.13 preferred the by-value one. Scala 3 reports an ambiguity.

//> using scala 3.10.0
import scala.language.implicitConversions

class Foo:
  def foo(): String = "foo"

implicit def boolFoo(bool: Boolean): Foo = new Foo
implicit def lazyBoolFoo(lazyBool: => Boolean): Foo = new Foo

@main def ambigDemo(): Unit =
  println(true.foo())
-- [E007] Type Mismatch Error: ambig.scala:11:10
11 |  println(true.foo())
   |          ^^^^
   |Found:    (true : Boolean)
   |Required: ?{ foo: ? }
   |Note that implicit extension methods cannot be applied because they are ambiguous;
   |both method boolFoo and method lazyBoolFoo provide an extension method `foo` on (true : Boolean)
1 error found

The temporary fix is to name the conversion, boolFoo(true).foo(), which prints foo. The lasting fix is to delete the by-name overload or to replace both with one extension method.

Other changed features

Eight rows describe features that Scala 3 simplified or restricted "to make the language easier, safer or more consistent". They have no deprecation on 2.13, and only the first has a migration rewrite.

Inheritance shadowing

An identifier used inside a class can name a member of an enclosing scope or an inherited member of the same name. Scala 2 silently preferred the inherited one, which depends on the parent’s code. Scala 3 asks you to say which.

//> using scala 3.10.0

class A:
  val x = 2

object B:
  val x = 1
  class C extends A:
    println(x)

@main def shadow2Demo(): Unit = new B.C
-- [E049] Reference Error: shadow2.scala:9:12
9 |    println(x)
  |            ^
  |            Reference to x is ambiguous.
  |            It is both defined in object B
  |            and inherited subsequently in class C
1 error found

The same error appears on 3.3.8 and 3.9.0. The rewrite writes println(this.x) (inherited) and B.x is the outer one. Choose deliberately: the rewrite assumes the Scala 2 meaning.

Non-private constructor in a private class

A constructor declared private[foo] in a private class leaks the private class in its own type signature.

//> using scala 3.10.0

package foo

object Foo:
  private class Bar private[foo] ()
-- Error: privctor.scala:6:19
6 |  private class Bar private[foo] ()
  |                   ^
  |      non-private constructor Bar in class Bar refers to private class Bar
  |      in its type signature (): foo.Foo.Bar
1 error found

Since the class is private, make the constructor private too. The migration guide says migration mode only warns about it; there is no automatic rewrite.

Abstract override

Re-declaring an inherited concrete method as abstract in a subtrait did nothing in Scala 2; in Scala 3 the subtrait’s version wins and subclasses must implement it.

//> using scala 3.10.0

trait A:
  def bar(x: Int): Int = x + 3

trait B extends A:
  def bar(x: Int): Int

class C extends B
-- [E231] Declaration Error: absover.scala:9:6
9 |class C extends B
  |      ^
  |class C needs to be abstract, since def bar(x: Int): Int in trait B is not defined
1 error found

Delete the abstract def in B; it had no effect on 2.13. The decision is recorded in Dotty issue 4770.

Case class companion

In Scala 2 the companion object of a case class extended FunctionN, so Foo.tupled, Foo.curried and Foo.andThen existed. Scala 3 companions do not extend those traits.

//> using scala 3.10.0

case class Foo(x: Int, b: Boolean)

@main def ccompDemo(): Unit =
  println(Foo.curried(1)(true))
  println(Foo.tupled((2, false)))
-- [E008] Not Found Error: ccomp.scala:6:14
6 |  println(Foo.curried(1)(true))
  |          ^^^^^^^^^^^
  |          value curried is not a member of object Foo
-- [E008] Not Found Error: ccomp.scala:7:14
7 |  println(Foo.tupled((2, false)))
  |          ^^^^^^^^^^
  |          value tupled is not a member of object Foo
2 errors found

Give the constructor a function type explicitly:

//> using scala 3.10.0

case class Foo(x: Int, b: Boolean)

@main def ccompFixDemo(): Unit =
  val fooCtr: (Int, Boolean) => Foo = Foo.apply
  println(fooCtr.curried(1)(true))
  println(fooCtr.tupled((2, false)))

Output: Foo(1,true) and Foo(2,false). The guide’s alternative (Foo.apply _).curried(1)(true) also cross-compiles but now carries the trailing-underscore warning. A named function value allocates once, which helps hot paths. See Case Classes.

Explicit call to unapply

The synthetic unapply of a case class returned Option[(A, B)] in Scala 2; Scala 3 returns the case class itself (the extractor is option-less, which avoids an allocation). Pattern matching is unaffected, and so are user-written extractors. Only a direct call changes:

//> using scala 3.10.0

case class Location(lat: Double, long: Double)

def tuple(location: Location): (Double, Double) =
  Location.unapply(location).get
-- [E008] Not Found Error: unap.scala:6:29
6 |  Location.unapply(location).get
  |  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |  value get is not a member of Location
1 error found

A pattern binding replaces the call:

//> using scala 3.10.0

case class Location(lat: Double, long: Double)

def tuple(location: Location): (Double, Double) =
  val Location(lat, lon) = location
  (lat, lon)

@main def unapFixDemo(): Unit =
  println(tuple(Location(1.5, 2.5)))
  println(Location.unapply(Location(3, 4)))

The output is (1.5,2.5) and Location(3.0,4.0), the second line showing that unapply now returns its argument. More on extractors in Extractors & Pattern Bindings.

Invisible bean property

The getter and setter that @BeanProperty generates exist in the bytecode (for Java frameworks) but are invisible to Scala 3 source.

-- [E008] Not Found Error: bean.scala:9:7
9 |  pojo.setFooBar("hello")
  |  ^^^^^^^^^^^^^^
  |  value setFooBar is not a member of Pojo
-- [E008] Not Found Error: bean.scala:10:15
10 |  println(pojo.getFooBar())
   |          ^^^^^^^^^^^^^^
   |          value getFooBar is not a member of Pojo
2 errors found

Use the idiomatic accessor, pojo.fooBar = "hello". The accessors are still generated, which a reflection listing of the class confirms:

//> using scala 3.10.0
import scala.beans.BeanProperty

class Pojo():
  @BeanProperty var fooBar: String = ""

@main def beanFixDemo(): Unit =
  val pojo = new Pojo()
  pojo.fooBar = "hello"
  println(pojo.fooBar)
  val names = classOf[Pojo].getMethods.map(_.getName).filter(_.endsWith("FooBar")).sorted
  println(names.mkString(", "))

prints hello and getFooBar, setFooBar. Java code that uses the JavaBeans naming sees them as before; see Calling Scala from Java.

=> T as a type argument

A by-name type cannot instantiate a type parameter. The standard library function Function.uncurried has such a case:

//> using scala 3.10.0

val f: Int => (=> Int) => Int = a => b => a + b
val g: (Int, => Int) => Int = Function.uncurried(f)
-- [E134] Type Error: byname.scala:4:39
4 |val g: (Int, => Int) => Int = Function.uncurried(f)
  |                              ^^^^^^^^^^^^^^^^^^
  |None of the overloaded alternatives of method uncurried in object Function with types
  | [T1, T2, T3, T4, T5, R]
  |  (f: T1 => T2 => T3 => T4 => T5 => R): (T1, T2, T3, T4, T5) => R
  ...
  |match arguments ((f² : Int => (=> Int) => Int))
1 error found

The fix depends on the situation: write your own uncurried with the by-name signature, or inline its two-line body.

Wildcard type argument on a higher-kinded member

Scala 3 cannot reduce the application of an abstract higher-kinded type member to a wildcard.

//> using scala 3.10.0

trait Example:
  type Foo[A]
  def f(foo: Foo[_]): Unit
-- Warning: wild.scala:5:17
5 |  def f(foo: Foo[_]): Unit
  |                 ^
  |`_` is deprecated for wildcard arguments of types: use `?` instead
  |This construct can be rewritten automatically under -rewrite -source 3.4-migration.
-- [E043] Type Error: wild.scala:5:13
5 |  def f(foo: Foo[_]): Unit
  |             ^^^^^^
  |unreducible application of higher-kinded type Example.this.Foo to wildcard arguments
1 warning found
1 error found

Both messages appear: the _ deprecation of 3.4 and the real error. Replace the wildcard with a type parameter (def f[A](foo: Foo[A])). When the member is itself an argument, as in Seq[Foo[_]], wrap it: class FooWrapper[A](foo: Foo[A]) and use Seq[FooWrapper[?]]. That fixed version compiles on 3.10:

//> using scala 3.10.0

trait Example:
  type Foo[A]
  def f[A](foo: Foo[A]): Unit
  class FooWrapper[A](foo: Foo[A])
  def g(foos: Seq[FooWrapper[?]]): Unit

Type checker

The Scala 2.13 type checker is unsound in a few specific cases that Scala 3 fixes, so some code that compiled and crashed at run time now fails at compile time.

Variance checks on default arguments and inner classes

Scala 2 did not check default arguments and inner classes for variance.

//> using scala 3.10.0

class Foo[-A](x: List[A]):
  def f[B](y: List[B] = x): Unit = ???

class Outer[+A](x: A):
  class Inner(y: A)
-- Error: variance.scala:4:24
4 |  def f[B](y: List[B] = x): Unit = ???
  |                        ^
  |contravariant type A occurs in covariant position in type [B]: List[A] of method f$default$1
-- Error: variance.scala:7:14
7 |  class Inner(y: A)
  |              ^^^^
  |covariant type A occurs in contravariant position in type A of parameter y
2 errors found

Each case needs care. The usual options are to make A invariant, to add a bound on B, or to add an overload instead of a default. Applying two of them:

//> using scala 3.10.0

class Foo[-A](x: List[A]):
  def f[B](y: List[B]): Unit = println(y)
  def f(): Unit = f(x)

class Outer[+A](x: A):
  class Inner[B >: A](y: B):
    def get: B = y

@main def varianceFixDemo(): Unit =
  Foo[Int](List(1, 2)).f()
  val o = Outer("a")
  println(o.Inner("b").get)

It prints List(1, 2) and b. As a stop-gap, @uncheckedVariance silences the check, but the unsoundness stays. Variance itself is the subject of Variance.

Pattern matching soundness

A pattern on two values can bind them to different type arguments of the same generic class. Scala 3 refuses to treat them as the same.

//> using scala 3.10.0

trait Request
case class Fetch[A](ids: Set[A]) extends Request

object Request:
  def combineFetch[A](x: Fetch[A], y: Fetch[A]): Fetch[A] = Fetch(x.ids ++ y.ids)

  def combineReq(x: Request, y: Request): Request =
    (x, y) match
      case (x @ Fetch(_), y @ Fetch(_)) => combineFetch(x, y)
-- [E007] Type Mismatch Error: patm.scala:11:59
11 |      case (x @ Fetch(_), y @ Fetch(_)) => combineFetch(x, y)
   |                                                           ^
   |                                              Found:    (y : Fetch[A$2])
   |                                              Required: Fetch[A$1]
1 error found

The compiler is right: nothing proves both Fetch values share a type argument. Relax the signature so that the argument types may differ as long as they have a common ancestor:

def combineFetch[A](x: Fetch[? <: A], y: Fetch[? <: A]): Fetch[A] = Fetch(x.ids ++ y.ids)

With that change, combineReq(Fetch(Set(1)), Fetch(Set("a"))) returns Fetch(Set(1, a)) (a Set[Int | String]), the case the old signature could never type-check. A cast would also compile but only moves the failure to run time.

Type inference

Scala 3 redesigned type inference, so a few programs infer a different type and a few get a new error. The guide recommends writing the result types of all public members; the Scalafix rule ExplicitResultTypes can do so before the migration.

Return type of an override

When an overriding method has no declared type, Scala 2.13 infers it from the right-hand side. Scala 3 takes it from the overridden method.

//> using scala 3.10.0

class Foo

class RichFoo(foo: Foo) extends Foo:
  def show: String = "rich"

class Parent:
  def foo: Foo = new Foo

class Child extends Parent:
  override def foo = new RichFoo(super.foo)

@main def overRetDemo(): Unit =
  println((new Child).foo.show)
-- [E008] Not Found Error: overret.scala:15:26
15 |  println((new Child).foo.show)
   |          ^^^^^^^^^^^^^^^^^^^^
   |          value show is not a member of Foo
1 error found

Child#foo returns RichFoo on 2.13 and Foo on 3. Declaring override def foo: RichFoo keeps the 2.13 meaning, and the program then prints rich. This is the second quiet change that can be a runtime difference (implicit conversions and casts could pick another path), so public overrides should always state their type.

Reflective type

Scala 2 inferred a structural type for new { def bar = ... } and allowed foo.bar through reflection (with import scala.language.reflectiveCalls). Scala 3 infers Object for the anonymous class.

//> using scala 3.10.0
import scala.language.reflectiveCalls

val foo = new { def bar: Int = 7 }

@main def reflDemo(): Unit =
  println(foo.bar)
-- [E008] Not Found Error: refl.scala:7:14
7 |  println(foo.bar)
  |          ^^^^^^^
  |          value bar is not a member of Object
1 error found

The fix is to write the structural type: val foo: { def bar: Int } = new { def bar: Int = 7 }. The program then prints 7, but with -deprecation the call produces a warning: method reflectiveSelectableFromLangReflectiveCalls in object Selectable is deprecated since 3.0: import scala.reflect.Selectable.reflectiveSelectable instead of scala.language.reflectiveCalls. Import scala.reflect.Selectable.reflectiveSelectable instead. Structural types, Selectable and Dynamic have their own page, Structural Types, Selectable & Dynamic.

Macros

The Scala 3 compiler cannot expand Scala 2.13 macros. A macro definition compiles to nothing it can use:

//> using scala 3.10.0
//> using dep org.scala-lang:scala-reflect:2.13.18
import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context

object M:
  def hello: String = macro helloImpl
  def helloImpl(c: Context): c.Expr[String] = ???
-- Error: macro2.scala:7:6
7 |  def hello: String = macro helloImpl
  |  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |  No Scala 3 implementation found for this Scala 2 macro.
1 error found

The Scala 2 macro system, including def macros, quasiquotes, TypeTag and runtime reflection through scala-reflect, is replaced by inline, scala.compiletime, quotes and splices, and quotes.reflect. A library may keep both implementations side by side in one artifact (the Scala 3 compiler uses the Scala 3 one), a technique called mixing. It serves Scala 2.13 users only if the artifact is built by Scala 3.7 or older, because Scala 2.13’s -Ytasty-reader supports Scala 3 libraries up to 3.7 and not from 3.8 on. Mixing and its alternative, cross-building one artifact per Scala version, are described in Migrating from Scala 2. The replacement features are introduced in Metaprogramming Overview and Macros: Quotes & Splices.

The reference’s dropped features

The language reference has a chapter of 17 dropped features. Eight overlap with the migration table; the table below maps all of them, and the sections after it cover the nine that the table does not.

Reference page Status on 3.10 Where on this page

Dropped: DelayedInit

semantics gone, App deprecated in 3.8.0

DelayedInit and App

Dropped: Scala 2 Macros

error

Macros

Dropped: Existential Types

error

Existential types

Dropped: General Type Projection

error on abstract types

General type projection

Dropped: Do-While

error

do-while

Dropped: Procedure Syntax

error

Procedure syntax

Dropped: Package Objects

still compile, no warning

Package objects

Dropped: Early Initializers

error

Early initializers

Dropped: Class Shadowing

error

Class shadowing

Dropped: Limit 22

lifted

The limit of 22

Dropped: XML Literals

compile only with scala-xml

XML literals

Dropped: Symbol Literals

error

Symbol literals

Dropped: Auto-Application

error

Auto-application

Dropped: Weak Conformance

gone

Weak conformance

Deprecated: Nonlocal Returns

warning

Nonlocal returns

Dropped: private[this] and protected[this]

warning

private[this] and protected[this]

Dropped: Wildcard Initializer

warning

Wildcard initializer

DelayedInit and App

The special handling of DelayedInit is gone. DelayedInit still exists and is deprecated since 2.11.0, but a class that extends it no longer has its constructor body passed to delayedInit:

//> using scala 3.10.0

trait Logger extends DelayedInit:
  def delayedInit(body: => Unit): Unit =
    println("before")
    body

class Job extends Logger:
  println("running")

@main def delayedDemo(): Unit =
  Job()

It compiles with a deprecation warning, and prints only running; in Scala 2 it printed before first. The consequence that surprises people is App, which was built on DelayedInit. The reference says it is "partially broken", and on 3.10 App is itself deprecated, since 3.8.0:

//> using scala 3.10.0

object Greeter extends App:
  println(s"args = ${args.length}")
-- Deprecation Warning: app.scala:3:23
3 |object Greeter extends App:
  |                       ^^^
  |trait App in package scala is deprecated since 3.8.0: Support for trait App is deprecated in Scala 3. Please refer to https://docs.scala-lang.org/scala3/book/methods-main-methods.html.
1 warning found
Exception in thread "main" java.lang.ExceptionInInitializerError
	at Greeter.main(app.scala)
Caused by: java.lang.NullPointerException: Cannot read the array length because the return value of "Greeter$.args()" is null
	at Greeter$.<clinit>(app.scala:4)

Two real consequences: the body runs in the object’s static initializer (on some JVMs it is only interpreted, so never benchmark inside an App), and args is null there, so reading it crashes. An App that never reads args still runs (a variant printing started ran fine). The replacements are @main or an explicit main:

//> using scala 3.10.0

@main def greet(names: String*): Unit =
  println(s"${names.length} names: ${names.mkString(", ")}")

With the arguments ada grace this prints 2 names: ada, grace. An object Hello { def main(args: Array[String]) } run with three arguments prints args = 3. See Hello World & @main Methods.

General type projection

T#A is only legal when T is a concrete type. On an abstract type, type parameter or alias to one, it is unsound (combined with other Scala 3 features) and it rules out the type-level encoding of a combinator calculus.

//> using scala 3.10.0

trait Holder:
  type A

def get[T <: Holder](a: T#A): T#A = a

object Allowed:
  class Outer:
    class In
  def ok(x: Outer#In): Outer#In = x
-- Error: tproj.scala:6:24
6 |def get[T <: Holder](a: T#A): T#A = a
  |                        ^
  |                        T is not a legal path
  |                        since it is not a concrete type
-- Error: tproj.scala:6:30
...
2 errors found

The Outer#In projection on the concrete class compiles. For abstract prefixes, use a path-dependent type (def get(h: Holder)(a: h.A): h.A) or an implicit parameter. The same rule explains why type lambdas written with the Scala 2 projection trick are unnecessary now: see Type Lambdas & Higher Kinds.

Package objects

The reference page says package objects "will be dropped", but they are only slated: they still work on 3.10, without a warning even under -deprecation.

//> using scala 3.10.0

package object trailhead:
  val maxGrade = 6

package trailhead:
  def describe: String = s"max grade $maxGrade"

@main def pkgObjDemo(): Unit =
  println(trailhead.describe)

prints max grade 6. The Scala 3 form is a top-level definition, in any file of the package. The compiler wraps top-level definitions in a synthetic object named after the file (src$package), so renaming such a file changes the binary name of what it contains. The 3.10 release notes say there is no concrete plan to remove package objects, and SIP-68, which lets referenceable package objects behave regularly, is a preview feature in 3.10. See Packages, Imports & Top-Level Definitions.

Class shadowing

An inner class cannot have the same name as an inner class of its parent. In Scala 2 the two looked like an override but classes cannot be overridden.

//> using scala 3.10.0

class Base:
  class Ops

class Sub extends Base:
  class Ops
-- [E069] Naming Error: shadowcls.scala:7:8
7 |  class Ops
  |        ^
  |class Ops cannot have the same name as class Ops in class Base -- class definitions cannot be overridden
1 error found

Rename one of the classes.

The limit of 22

Function types and tuples are no longer limited to 22 parameters or fields. Beyond 22 they erase to scala.runtime.FunctionXXL and scala.runtime.TupleXXL, implemented with arrays. A 23-field tuple and a 23-parameter function, generated by script, compile and run:

//> using scala 3.10.0

@main def limitDemo(): Unit =
  val big = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23)
  println(big.size)
  println(big(22))
  val sum: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) => Int =
    (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23) =>
      a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12 + a13 + a14 + a15 + a16 + a17 + a18 + a19 + a20 + a21 + a22 + a23
  println(sum(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1))

The output is 23, 23 and 23. This is a removal of a limit, not an incompatibility, but code that worked around it with nested tuples can now be flattened. See Tuples & Named Tuples.

XML literals

Inline XML (<a>b</a>) in Scala source is on its way out: the reference says literals "will be dropped in the near future" in favor of string interpolation. On 3.10 they still parse, but the compiler needs the scala-xml library and reports a missing package otherwise:

-- [E008] Not Found Error: xmlnodep.scala:4:10
4 |  val x = <a>b</a>
  |          ^
  |          value xml is not a member of scala
1 error found

With //> using dep org.scala-lang.modules::scala-xml:2.4.0 the program val x = <peak><name>{name}</name></peak> prints <peak><name>Pico</name></peak>. The hint printed by Scala CLI at that point recommended 2.5.0, which was the newest version on Maven Central at the check date. New code should build documents with scala.xml constructors, a template library or the string interpolators of an XML library.

Weak conformance

Scala 2 used weak conformance to widen numeric types, so List(n, c, d) with an Int, a Char and a Double was a List[Double]. Scala 3 keeps one rule: an Int literal adapts to another numeric type when needed.

//> using scala 3.10.0

@main def weakDemo(): Unit =
  val n: Int = 3
  val c: Char = 'X'
  val d: Double = math.sqrt(3.0)
  val literals: List[Double] = List(1.0, math.sqrt(4.0), 0, -3.3)
  val mixed = List(n, c, d)
  val asDoubles: List[Double] = mixed
  println(literals)
-- [E007] Type Mismatch Error: weak.scala:9:32
9 |  val asDoubles: List[Double] = mixed
  |                                ^^^^^
  |                             Found:    (mixed : List[Int | Char | Double])
  |                             Required: List[Double]
1 error found

Without the last assignment the program prints List(1.0, 2.0, 0.0, -3.3): the literal 0 adapts to Double. On 3.10 the type of the mixed list is the union List[Int | Char | Double], not even List[AnyVal] as the reference says (the exact printed type is more precise than the documentation). Add a type ascription or convert the values (n.toDouble).

Nonlocal returns

A return inside a lambda used to unwind through an exception, NonLocalReturnException, with a hidden cost and a leaky failure mode (a catch of Exception would intercept it). It is deprecated since 3.2.0 and warns on 3.10 without any flag:

//> using scala 3.10.0

def firstIndex[T](xs: List[T], elem: T): Int =
  xs.zipWithIndex.foreach { (x, i) =>
    if x == elem then return i
  }
  -1

@main def nonlocalDemo(): Unit =
  println(firstIndex(List("a", "b", "c"), "b"))
-- Warning: nonlocal.scala:5:22
5 |    if x == elem then return i
  |                      ^^^^^^^^
  |Non local returns are no longer supported; use `boundary` and `boundary.break` in `scala.util` instead
1 warning found
1

The replacement is scala.util.boundary, which also replaces the Breaks API. It is explicit, lexically scoped and checked by the compiler:

//> using scala 3.10.0
import scala.util.boundary, boundary.break

def firstIndex[T](xs: List[T], elem: T): Int =
  boundary:
    for (x, i) <- xs.zipWithIndex do
      if x == elem then break(i)
    -1

@main def boundaryDemo(): Unit =
  println(firstIndex(List("a", "b", "c"), "b"))
  println(firstIndex(List("a", "b", "c"), "z"))

The output is 1 and -1. See Exceptions, boundary & Early Exit.

private[this] and protected[this]

The compiler now infers that a private member only accessed through this is object-private, which was the purpose of private[this]. The qualified form is deprecated, and protected[this] is dropped without replacement. On 3.10 the old form compiles with a warning:

-- Warning: privthis.scala:4:9
4 |  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.
1 warning found

The rewrite drops the qualifier. The subtle part is the second runtime incompatibility: a private val that is only used through this and by no method may no longer generate a field, so reflection that expected the field fails. The fix is the qualified form with the class as qualifier, private[C] val field = x + 1. Class parameters declared private val keep their fields and variance checks.

Source: Scala 3 Reference: Dropped: private[this and protected[this]]

Wildcard initializer

var x: A = _ declared an uninitialized field. The marker is now a value in scala.compiletime:

//> using scala 3.10.0
import scala.compiletime.uninitialized

class Cell:
  var x: Int = uninitialized
  var name: String = uninitialized

@main def wildInitFixDemo(): Unit =
  val c = Cell()
  println((c.x, c.name))

The output is (0,null). The old = _ compiles with the warning = _ has been deprecated; use = uninitialized instead and is rewritten by -source:3.4-migration.

From warnings to errors: the 3.3 to 3.10 timeline

Scala 3.0 to 3.3 kept several Scala 2 forms alive for migration. From 3.4 they warned, and 3.10 removed the first of them. So an old Scala 3 source can also fail on 3.10. The table gives the severity of each form on the three compilers tried here; every cell is a measured result, not documentation.

Form 3.3.8 3.9.0 3.10.0 Replacement

A with B as a type

silent

warning

error

A & B

xs: _* vararg splice

silent

warning

warning

xs*

List[_] wildcard type argument

silent

warning

warning

List[?]

private[this]

silent

warning

warning

private

var x: Int = _

silent

warning

warning

= uninitialized

trailing f _ on a method

silent

warning

warning

f

for Some(x) <- xs refutable generator

warning

error

error

for case Some(x) <- xs

this in a top-level definition

silent

silent

error

remove it

given in an inaccessible companion

silent

warning with -deprecation

not found

public companion

-> through an implicit class

custom

custom

Predef wins

extension

RichInt and ArrowAssoc

not run

not run

deprecated

extension methods

The intermediate releases 3.4 to 3.8 were not run; the 3.9 release notes say that -source:3.4-migration -rewrite applies every one of the 3.4 rewrites.

A with B becomes an error

The Scala 2 compound type has been deprecated in favor of & since 3.4. On 3.10:

-- [E003] Syntax Error: with2.scala:5:15
5 |def f(x: Named with Sized): Unit = ()
  |               ^^^^
  |with as a type operator has been deprecated; use & instead
  |This construct can be rewritten automatically under -rewrite -source 3.4-migration.
-- [E003] Syntax Error: with2.scala:7:17
7 |  case y: (Named with Sized) => ()
...
2 errors found

The identical file on 3.9.0 reports two warnings under the same code E003, and 3.3.8 compiles silently. In a typed pattern the intersection needs parentheses, case y: (A & B); the rewrite adds them. with remains valid in class C extends A with B (a different construct). The 3.10 release notes name this among the removed legacy features that end cross-compilation between Scala 2 and 3.10.

The 3.4 deprecations in one rewrite

The remaining items of the 3.4 list (_ type wildcards, xs: _*, private[this], var x = _ and the trailing _) are all fixed at once by -source:3.4-migration -rewrite. Applied to a class with those forms it produces exactly:

-  private[this] var n = 0
+  private var n = 0
-  var cache: Int = _
+  var cache: Int = scala.compiletime.uninitialized
-  def label(x: Named with Sized): String = "x"
+  def label(x: Named & Sized): String = "x"
-  def go(): Int = { val ys: List[_] = Nil; sum(List(1, 2): _*) + ys.size }
+  def go(): Int = { val ys: List[?] = Nil; sum(List(1, 2)*) + ys.size }
-  val g = twice _
+  val g = twice

The compiler reported six warnings and patched the file. A later flag does not replay an earlier one, so the migration to a newer LTS is a sequence of -source:3.4-migration, 3.5-migration and so on. That recipe is on Upgrading Between Scala 3 Versions.

Refutable patterns in for generators

A generator pattern that is narrower than the type of the source needs an explicit case, because an implicit filter hid data loss:

-- Error: refut.scala:5:14
5 |  val a = for Some(x) <- xs yield x
  |              ^^^^^^^
  |pattern's type Some[Int] is more specialized than the right hand side expression's type Option[Int]
  |
  |If the narrowing is intentional, this can be communicated by adding the `case` keyword before the full pattern,
  |which will result in a filtering for expression (using `withFilter`).
  |This patch can be rewritten automatically under -rewrite -source 3.2-migration.
1 error found

The same line is only a warning on 3.3.8 and is an error on 3.9.0 and 3.10.0. for case Some(x) <- xs yield x returns List(1, 3). The mechanics of generators are in For Comprehensions.

Givens in an inaccessible companion

When the compiler looks for an implicit in a type’s companion it now only considers instances accessible at the call site.

//> using scala 3.10.0

object api:
  class Config
  private object Config:
    given Config = new Config
  def describe(using Config): String = "configured"

@main def privCompDemo(): Unit =
  println(api.describe)
-- [E172] Type Error: privcomp.scala:10:22
10 |  println(api.describe)
   |                      ^
   |No given instance of type api.Config was found for parameter x$1 of method describe in object api
1 error found

3.3.8 prints configured silently; 3.9.0 prints it with a deprecation warning under -deprecation: Usage of implicit given instance given_Config defined in object Config, which is not accessible here. In Scala 3.10, this implicit will no longer be found. The library-side fix is a public companion or a given in a public object. Users usually cannot work around it, so publish the fix before your consumers move to 3.10.

this in a top-level definition

Before 3.10, val self = this at the top of a file resolved to the synthetic package object of the file. That was almost never intended:

-- Error: topthis.scala:3:11
3 |val self = this
  |           ^^^^
  |           Top-level definitions cannot refer to this
1 error found

It compiles on 3.9.0 and 3.3.8. Remove the reference or move the definition into an object.

-> is now an extension method

The standard a -> b pair syntax became an inline extension method on 3.10, and Predef.ArrowAssoc stopped being an implicit conversion. That can silently change which -> runs. A DSL that defines -> through an implicit class loses to the standard one:

//> using scala 3.10.0

object dsl:
  implicit class Arrow[A](a: A):
    def ->[B](b: B): String = s"custom($a, $b)"

import dsl.*

@main def arrow2Demo(): Unit =
  println(1 -> "a")

On 3.10.0 this prints the tuple (1,a). The same file prints custom(1, a) on 3.9.0 and 3.3.8. No error, no warning: the program compiles and the meaning changes. The version defined as an extension method keeps its priority and prints custom(1, a) on 3.10 when dsl.* is imported. So a DSL that ships its own -> has to be rewritten as an extension. See Domain Specific Languages.

Using the old names directly produces deprecation warnings, both since 3.10.0:

class RichInt in package scala.runtime is deprecated since 3.10.0: use the extension methods available on primitive types instead
method ArrowAssoc in object Predef is deprecated since 3.10.0: Use `->` extension method instead.

Ordinary uses (1 to 3, 3.max(4), 5.abs) are unaffected. Macros and compiler plugins that inspect the old Rich* trees must match the new shape.

Strictness that is still opt-in

Two checks that Scala 3 introduced are not errors by default on 3.10. An alphanumeric method that is not declared infix and is used as an infix operator warns by default since 3.4 (3.3.8 accepts it silently), and the warning becomes an error only under -source:future. The Matchable constraint on a type test of an Any value is silent by default and warns only under -source:future. The program:

//> using scala 3.10.0

case class Pair(n: Int):
  def plus(m: Int): Int = n + m

@main def run(): Unit =
  val x: Any = 3
  println(Pair(1) plus 2)
  x match
    case s: String => println(s)
    case i: Int => println(i)

Compiled as it is, it prints 3 twice after one warning:

-- Warning: infix.scala:8:18
8 |  println(Pair(1) plus 2)
  |                  ^^^^
  |Alphanumeric method plus is not declared infix; it should not be used as infix operator.
  |Instead, use method syntax .plus(...) or backticked identifier `plus`.
  |The latter can be rewritten automatically under -rewrite -source 3.4-migration.
1 warning found

With //> using options -source:future the same use is an error, and both type tests warn:

-- Error: infix.scala:8:18
8 |  println(Pair(1) plus 2)
  |                  ^^^^
  |Alphanumeric method plus is not declared infix; it should not be used as infix operator.
  |Instead, use method syntax .plus(...) or backticked identifier `plus`.
  |The latter can be rewritten automatically under -rewrite -source 3.4-migration.
-- [E165] Type Warning: infix.scala:10:12
10 |    case s: String => println(s)
   |            ^^^^^^
   |            pattern selector should be an instance of Matchable,
   |            but it has unmatchable type Any instead
-- [E165] Type Warning: infix.scala:11:12
11 |    case i: Int => println(i)
   |            ^^^
   |            pattern selector should be an instance of Matchable,
   |            but it has unmatchable type Any instead
2 warnings found
1 error found

Declare such methods infix and keep values in a Matchable-typed variable, to be ready for the day these become the default. The infix warning is also covered, with the style guide’s view of it, on Style Guide. See Type Hierarchy & Matchable.

Three Scala 2 forms look like candidates for removal but behave as follows on 3.10:

  • Type lambdas written with a projection compile: lift[({ type L[x] = List[x] })#L](List(1)) is accepted, because the prefix is a concrete refinement. The Scala 3 form is [x] =>> List[x]. The kind-projector syntax Either[String, *] is not accepted by default (Not found: type *); it needs -Xkind-projector, which then prints Right(1) for lift[Either[String, *]](Right(1)) and Right(2) for lift[[x] =>> Either[String, x]](Right(2)).

  • Implicit conversions and implicit classes still compile and run. implicit def intToString(i: Int): String followed by val s: String = 42 prints 42, with the feature warning Definition of implicit conversion method intToString should be enabled by adding the import clause 'import scala.language.implicitConversions'. Implicit classes stay, too ("hi".shout printed HI!), though extension is preferred. Only the resolution rules changed.

  • Deprecated Scala 2.13 library names still compile with warnings: Traversable (use Iterable), Stream (use LazyList), CanBuildFrom (use BuildFrom), JavaConverters (use scala.jdk.CollectionConverters) and the strict Map.mapValues (use .view.mapValues(f), then .toMap). The replacement program runs and prints (6,List(1, 2, 3),Map(1 -> 3),List(1, 2)).

Type lambdas have their own page, Type Lambdas & Higher Kinds.

Where the books are outdated

The three books consulted for this section are Programming Scala 3rd edition (abbreviated PS3), Learn Scala Programming (LSP) and Professional Scala (PRO). Each was written before some of the changes above, and the table collects every outdated form with the current state on 3.10. Chapter references are the issue’s own mapping. The table contains no book text, only the forms and their current replacement.

Book statement or form Current Scala 3.10

implicit val/def instances, implicit parameters, implicitly (LSP ch. 4, 7-10; PRO ch. 6)

given, using and summon. Scala 2 implicits still compile, for migration. An implicit definition without a result type is an error (Type of an implicit definition).

implicit class for extension methods (LSP ch. 4, 9, 10; PRO ch. 6, 8)

extension methods. Implicit classes compile, but a -> defined through one now loses to Predef (-> is now an extension method).

implicit def conversions (LSP ch. 4; PRO ch. 6)

given Conversion[A, B] plus import scala.language.implicitConversions; into became stable in 3.9.

View bounds A <% B (LSP ch. 4; PRO ch. 5-6)

Removed (View bounds). Use a using conversion or a type class.

PS3’s given ... with forms and anonymous context bounds (ch. 5, 6, 16, 18)

The 3.6 given syntax (SIP-64): given Ord[Int]:, given [T: Ord] => Ord[List[T]]:, named and aggregate context bounds, deferred givens. The old form still compiles.

Given resolution examples (PS3 ch. 5; LSP ch. 4)

3.7 prefers the most general given; 3.10 no longer finds givens in inaccessible companions (Givens in an inaccessible companion).

PS3 ch. 16 view-bound example written with (given view: A => B)

Pre-3.0 Dotty syntax and an error in the book. It is (using ...), and a using function parameter is not applied as a conversion (Implicit views).

PS3 predicts that legacy syntax warns or fails "in 3.1": _ wildcards, : _*, alphanumeric infix, old implicits under * imports

Warnings arrived in 3.4 (_ wildcard, xs: _*, with as a type operator, private[this], var x = _, trailing f _). A with B is an error in 3.10. Alphanumeric infix warns by default since 3.4 and is an error only under -source:future; the Matchable check is still -source:future only (From warnings to errors: the 3.3 to 3.10 timeline).

-language:experimental.fewerBraces (PS3 ch. 2)

Standard since 3.3. On 3.10 the option only prints a deprecation: "`fewerBraces` is now standard, no language import is needed".

-Ysafe-init (PS3 ch. 12)

-Wsafe-init. On 3.10 -Ysafe-init prints bad option '-Ysafe-init' was ignored, while -Wsafe-init reports Access non-initialized value y for a class that reads a later val.

scala -classpath ... as the runner; cs install scala scalac scaladoc (PS3 ch. 1, 22)

Since 3.5 scala is Scala CLI (scala run, scala repl, scala test, //> using directives). Ammonite support was dropped in 3.9 (3.9 release notes).

JDK 8+ with 11 or 15 recommended (PS3 ch. 1); JDK 8 (PRO); JDK 8-11 (LSP)

Scala 3.8+ needs JDK 17+; the 3.3 LTS still runs on JDK 8. See Versions, Compatibility & JDKs.

"The Scala 3 standard library is the 2.13 library" (PS3 ch. 1, 14)

Same API, but since 3.8 it is compiled by Scala 3; the REPL is a separate artifact (scala3-repl).

Polymorphic function types cannot take context bounds or using (PS3 ch. 17)

They can in recent 3.x: type Sorter = [T] => List[T] => Ordering[T] ?=> List[T] and [T <: Int] => (xs: List[T]) => xs.headOption both compile and run on 3.10.

Compound types A with B (LSP ch. 2; PRO ch. 5)

A & B; with is an error in 3.10 (A with B becomes an error).

Existential types T forSome { ... } (LSP ch. 2; PRO ch. 5)

Dropped (Existential types). Use ? wildcards or type members.

List[_], Seq[_] (LSP, PRO)

List[?]; the underscore warns since 3.4.

Type lambdas ({ type L[x] = F[x] })#L with kind-projector 0.9 (LSP ch. 2, 8, 10)

[x] =>> F[x]. The projection form still compiles; -Xkind-projector is for migration only (Type Lambdas & Higher Kinds).

Type projections T#A on an abstract T (LSP ch. 2; PRO ch. 5)

Allowed only on concrete prefixes (General type projection).

Procedure syntax def f() { ... }; do { ... } while (c); symbol literals 'sym (PRO ch. 5, 6; PS3 ch. 2)

All dropped (Procedure syntax, do-while, Symbol literals). Use def f(): Unit = ..., the loop-and-a-half while, and Symbol("sym").

object Main extends App (PRO ch. 7)

DelayedInit semantics are dropped and App is deprecated since 3.8.0; args is null in the body. Prefer @main (DelayedInit and App).

Traversable, CanBuildFrom, Stream, strict mapValues, JavaConverters (PRO ch. 4, 6; LSP ch. 8)

The 2.13 hierarchy (IterableOnce/Iterable), BuildFrom/Factory, LazyList, .view.mapValues, scala.jdk.CollectionConverters.

“Option` extends `Traversable” (PRO ch. 5)

Wrong even in 2.12, where an implicit conversion applied. Option is an IterableOnce since 2.13.

mutable.MultiMap (LSP ch. 1); 2.13 milestone API names (LSP ch. 1)

MultiDict in scala-collection-contrib. Check every name against the final 2.13 API.

Scala 2 def macros, quasiquotes, scala.reflect runtime reflection, TypeTag, Shapeless derivation (PRO ch. 8)

inline, scala.compiletime, quotes and splices, quotes.reflect and Mirror-based derives. Scala 2 macros cannot be expanded by Scala 3 (Macros).

Structural types work out of the box (PRO ch. 5)

Reflective access needs import scala.reflect.Selectable.reflectiveSelectable (Reflective type).

Akka as the default actor and streams toolkit (LSP ch. 11-14; PRO ch. 7; PS3 ch. 19)

Akka has been under the Business Source License since 2.7 (2022). The Apache-licensed fork is Apache Pekko; Cats Effect, ZIO and Ox are common alternatives. Ecosystem page only.

ActorMaterializer, classic remoting, PersistentActor, bindAndHandle, Blaze, Cats Effect 2 (LSP ch. 11-14)

Out of scope; named here so that readers of the book know these APIs changed.

Lagom and ConductR (LSP ch. 15)

End of life (Lagom 2024-07-01). Not documented.

sbt 1.1-1.2, ENSIME, Ammonite, Mill under lihaoyi/mill (all three books)

sbt 1.x and sbt 2.0; Metals or IntelliJ; Scala CLI; Mill under com-lihaoyi.

ScalaTest 3.0 FunSuite/WordSpec imports (LSP, PRO)

ScalaTest 3.2 funsuite.AnyFunSuite and similar; MUnit is the Toolkit’s test framework.

"Dotty" as the future compiler (PRO, LSP)

Scala 3.0 shipped on 2021-05-14.

SPP, the Scala Platform Process (PRO ch. 8)

Defunct. Use SIPs and the Scala Toolkit.

Comparing with other languages

Scala is not alone in removing forms after years of deprecation. The path Scala 3.4 to 3.10 followed, a warning that names its replacement, a rewrite switch, and an error only a few minor releases later, can be compared with the breaking changes that Groovy collects release by release in Groovy: What’s New & Migrating from Older Versions. A different kind of constraint is the Java floor: Scala 3.8 and later need JDK 17, and the Maven release setting and toolchains that pin a build to a Java version are covered in Java Build and Tooling. Calling Java from Scala is covered in Using Java from Scala.

Pitfalls

  • A green 2.13 build is not a green Scala 3 build. Only about a third of the 32 rows have a 2.13 deprecation or feature warning; the rest (open brace, wrong indentation, inheritance shadowing, abstract override, case-class companions, variance checks and the type-checker fixes) fail only on Scala 3.

  • The rewrite assumes the Scala 2 meaning. this.x for inheritance shadowing, Symbol("abc") for a literal and () => x for value eta-expansion all keep the old behavior, even where the old behavior was the bug. Review the diff.

  • -source:3.0-migration will not run on brace-and-indent mixes. Rewriting a file that already uses Scala 3 optional braces together with Scala 2 constructs can fail with new syntax errors (it did here with def print() { inside a braceless object); run the rewrite on the Scala 2 style file first and convert to braceless afterwards.

  • Silent behavior changes need tests. Four changes do not produce an error: the inferred type of an override (Return type of an override), an implicit view resolved to a different conversion (Implicit views), -> through an implicit class (-> is now an extension method) and a private[this] field that is not generated (private[this] and protected[this]).

  • Binary compatibility is separate from source compatibility. 3.10 keeps loading libraries compiled with older Scala 3 versions that used the removed features; only the source of the library being compiled has to change. A 3.10-compiled library cannot be used by a project on 3.9 (see Versions, Compatibility & JDKs).

  • Libraries that must stay cross-compilable with Scala 2 should stay on the 3.9 LTS. The 3.10 release notes expect cross-compiled libraries to remain on the LTS indefinitely, and recommend 3.9 LTS for the longest support window.

  • Check MiMa. After a migration, run MiMa 1.2.1 or newer against the previous artifact: the removed features can change generic signatures, and the 3.10 notes mention IncompatibleSignatureProblem reports as an example.

What was run and what was not

All the [source,scala] programs marked with //> using scala 3.10.0 were compiled with Scala CLI 1.17.1 (using scala run for the @main programs and scala compile where there is no entry point), and their diagnostics and outputs are the ones printed above. The -rewrite experiments were run with --scalac-option -rewrite --scalac-option -source:3.0-migration and -source:3.4-migration on copies of the sources. The version comparisons used 3.3.8 and 3.9.0 for with, the vararg and wildcard forms, the refutable generator, the top-level this, the inaccessible companion given, the -> priority and inheritance shadowing, and 2.13.18 for the legacy program. The scala-xml dependency (2.4.0) and the scala-reflect dependency (2.13.18) were fetched from Maven Central.

Not run: the Scalafix rules (ExplicitResultTypes, Any2StringAdd and others), because they need sbt or the Scalafix CLI; sbt-scala3-migrate; and the Scala 2 macro cross-building recipes. These are described from the migration guide and are covered in Migrating from Scala 2. The claims about the intermediate releases 3.4 to 3.8 come from the release notes and were not run on those compilers.

References