Optional Braces & Indentation

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.0 (released 2026-10-12) and every example was run with Scala CLI 1.17.1 on JDK 26 (checked on 2026-10-15). Scala 3 lets a program drop most braces: a block is delimited by indentation, much as in Python, while braces keep working and can be mixed freely with indentation. The two styles produce the same program, so this is a choice of syntax and not a change of meaning. Every feature on this page except relaxed lambda syntax is stable on the 3.3 LTS and the 3.9 LTS lines as well: indentation syntax and the quiet control syntax arrived with Scala 3.0, and "fewer braces" (a colon in place of braces around a method argument) was experimental from 3.1 and became stable in 3.3. Relaxed lambda syntax is a preview feature in 3.10 (experimental in 3.9) and is covered in its own section at the end.

The examples follow the trailhead scenario used across this section, a toolkit that plans hikes on a network of mountain trails. The matching control-flow topics are on Conditionals & Loops, For Comprehensions and Pattern Matching; the formatting conventions that go with this syntax are on Style Guide.

The big picture

Three separate mechanisms are involved, and it helps to keep them apart.

Mechanism What it changes Turned off or rewritten with

Significant indentation

{ } around blocks, template bodies and arguments become optional

-no-indent, -indent, -rewrite

Quiet control syntax

parentheses around if/while conditions and for generators become optional, replaced by then and do

-new-syntax, -old-syntax, -rewrite

End markers

an optional end line names the construct that just closed

none (they are ordinary syntax)

Significant indentation is on by default for every file compiled with Scala 3. The compiler inserts virtual <indent> and <outdent> tokens at line breaks, and a pair of those behaves exactly like a pair of braces. The rule is therefore that adding a pair of optional braces never changes the meaning of a well-indented program.

The indentation rules

Independently of whether braces are optional, the compiler flags badly indented programs as warnings. There are two rules:

  • In a brace-delimited region, no statement may start to the left of the first statement after the opening brace that starts a new line. This catches a missing closing brace.

  • When significant indentation is turned off (Scala 2 mode or -no-indent) and an indented sub-part of an expression ends in a newline, the next statement must start at an indentation width less than the sub-part. This catches a missing opening brace.

//> using scala 3.10.0
@main def run(): Unit = {
  val x = 1
  if (x > 0) {
      println(1)
    println(2)
  println("done")
  }
}
[warn] ./w1.scala:6:5
[warn] Line is indented too far to the left, or a '}' is missing
[warn]     println(2)
[warn]     ^
[warn] ./w1.scala:7:3
[warn] Line is indented too far to the left, or a '}' is missing
[warn]   println("done")
[warn]   ^
1
2
done

The program still compiles and runs, because the braces are the authority here. The second rule has its own warning:

//> using scala 3.10.0
//> using option -no-indent
@main def run(): Unit = {
  val x = -1
  if (x < 0)
    println(1)
    println(2)
}
[warn] ./w2.scala:7:5
[warn] Line is indented too far to the right, or a '{' is missing before:
[warn]
[warn] println(1)
[warn]     println(2)
[warn]     ^
1
2

Both rules leave wide freedom. They say nothing about indentation inside an expression, and they do not require every statement of a block to line up exactly.

Where an indentation region can start

An <indent> is inserted at a line break when two things are both true: an indentation region can start at the current position, and the first token on the next line is indented strictly more than the current indentation width. A region can start:

  • after one of the tokens =, =>, ?=>, <-, catch, do, else, finally, for, if, match, return, then, throw, try, while and yield;

  • after the closing parenthesis of the condition of an old-style if or while, and after the closing parenthesis or brace of the enumerators of an old-style for without do;

  • after the leading parameters of an extension, and after with in a given instance;

  • after a colon that starts a template body or that stands where a method argument is expected (the next two sections).

An <outdent> is inserted when the first token on the next line is indented strictly less than the current width, unless the previous line ends with a token that shows the statement continues (then, else, do, catch, finally, yield, match). Several outdents can be inserted in a row. After an outdent the new line must match the indentation of some earlier line of the enclosing region. Indentation tokens are inserted only where the compiler also infers newline separators: at the top level and inside braces, but not inside parentheses, patterns or types.

Colon for template bodies

The body of a class, trait, object, enum, given instance, type refinement or package can be written as a colon at the end of the header followed by indented definitions. The colon is the ordinary colon; the compiler reads it as a template-body opener only where the grammar allows one and only after an identifier, a backticked identifier, this, super, ) or ].

//> using scala 3.10.0

package trailhead:
  enum Difficulty:
    case Easy, Hard

  trait Rated:
    def rating: Int

  class Trail(val name: String, val km: Double) extends Rated:
    def rating: Int = if km > 10 then 3 else 1

  object Trail:
    def apply(name: String, km: Double): Trail = new Trail(name, km)

  given Ordering[Trail] = Ordering.by(_.km)

  extension (t: Trail)
    def label: String = s"${t.name} (${t.km} km)"

@main def run(): Unit =
  import trailhead.{*, given}
  val anon = new Rated:
    def rating = 7
  val t = Trail("Pico", 12.5)
  println(t.label)
  println(anon.rating)
  println(t.rating)
  val sorted = List(Trail("A", 9), Trail("B", 2)).sorted
  println(sorted.map(_.name))
Pico (12.5 km)
7
3
List(B, A)

A few details are worth remembering:

  • In each case the trailing : can be replaced, without change of meaning, by braces around the indented definitions.

  • A class or object with no members needs no colon at all: class Marker, object Empty.

  • new Rated: followed by an indented body creates an anonymous class. In Scala 2 the same code needs new Rated { ... }.

  • The old given Show[Trail] with form still works on 3.10 (a trailing with also opens a region), but the preferred form is the colon. Under -source:future the compiler rejects it with the message "Given member definitions starting with with are no longer supported; use {...} or : followed by newline instead" (the form is still accepted by default on 3.10.0). Anonymous givens are covered on Givens & Using Clauses.

  • The members of the package are indented only when the package clause uses a colon. The common file-level form package trailhead on its own line (without a colon) keeps all the file’s definitions at column 0.

Quiet control syntax: then and do

The "quiet" syntax removes parentheses around conditions and generators. Where a keyword would otherwise be followed by a parenthesised condition, Scala 3 accepts a bare condition if it is followed by then (for if) or do (for while and for). A for that ends in yield produces a value, and one that ends in do is a loop.

//> using scala 3.10.0
import scala.util.control.NonFatal

@main def run(): Unit =
  val kms = List(12.5, 4.0, 8.0)
  // if / then / else
  val km = kms.head
  val kind =
    if km > 10 then "long"
    else if km > 5 then "medium"
    else "short"
  println(kind)
  // while / do
  var left = 3
  while left > 0 do left -= 1
  println(left)
  // for ... do, for ... yield, guards
  for k <- kms if k > 5 do println(k)
  val doubled = for k <- kms yield k * 2
  println(doubled)
  // multi-line generators
  for
    k <- kms
    m <- List(1, 2)
    if k > 5
  do
    println(s"$k x $m")
  // try / catch with one case on the same line, then several in an indented block
  val n = try "abc".toInt catch case _: NumberFormatException => -1
  println(n)
  val m =
    try Integer.parseInt("x1")
    catch
      case _: NumberFormatException => -2
      case NonFatal(e)              => -3
    finally println("parsed")
  println(m)
  // old syntax still compiles
  if (km > 10) println("old style")
  for (k <- kms) { if (k > 100) println(k) }
long
0
12.5
8.0
List(25.0, 8.0, 16.0)
12.5 x 1
12.5 x 2
8.0 x 1
8.0 x 2
-1
parsed
-2
old style

The rules in detail:

  • The condition of an if can be written without parentheses if it is followed by then.

  • The condition of a while can be written without parentheses if it is followed by do.

  • The generators of a for can be written without parentheses or braces if they are followed by yield or do.

  • A do after the generators means a loop. In Scala 2 the loop form was for (x <- xs) body and the value form was for (x <- xs) yield body.

  • A catch can be followed by a single case on the same line. With several cases they go in braces (as in Scala 2) or in an indented block.

Leaving out then on a braceless condition is an error, and the message is not obvious, because the compiler first reads println("positive") as the rest of the condition:

//> using scala 3.10.0
@main def run(): Unit =
  val x = 1
  if x > 0 println("positive")
[error] ./e3.scala:5:1
[error] 'then' expected, but eof found
[error] ./e3.scala:4:6
[error] value println is not a member of Boolean
[error]   if x > 0 println("positive")
[error]      ^^^^^^^^^^^^^

The parenthesised forms if (x > 0) ... and for (k <- ks) ... remain legal in Scala 3 unless you compile with -new-syntax.

Fewer braces: a colon instead of braces around an argument

Since Scala 3.3 (experimental in 3.1 and 3.2), a colon at the end of a line is also recognised where a function argument is expected. The indented block that follows becomes the last argument of the call. This is what makes control-like methods read as built-in syntax: repeat(2):, Using.resource(...): r =>, xs.map: x =>. The colon can be followed on the same line by the parameter part and the arrow of a lambda.

//> using scala 3.10.0
import scala.util.Using

case class Trail(name: String, km: Double)

def timed[T](label: String)(body: => T): T =
  val result = body
  println(s"$label done")
  result

def repeat(n: Int)(body: => Unit): Unit =
  for _ <- 1 to n do body

@main def run(): Unit =
  val trails = List(Trail("Pico", 12.5), Trail("Lago", 4.0), Trail("Cumbre", 8.0))

  // a by-name block after ':'
  repeat(2):
    println("ah")
    println("ha")

  // a lambda, parameter on the same line as the colon
  val hours = trails.map: t =>
    val h = t.km / 4
    h + 0.5
  println(hours)

  // the lambda parameter on its own, and a lambda with two parameters
  val total = trails.foldLeft(0.0): (acc, t) =>
    acc + t.km
  println(total)

  // partial function: a block of cases after ':'
  val labels = trails.map:
    case Trail(n, km) if km > 10 => s"$n: long"
    case Trail(n, _)             => s"$n: short"
  println(labels)

  // chaining: a leading '.' may sit at an odd indentation
  val names = trails
    .filter: t =>
      t.km > 5
    .map: t =>
      t.name.toUpperCase
  println(names)

  // method with a second parameter list
  val r = timed("plan"):
    trails.map(_.km).sum
  println(r)

  // ': ' after an infix operator
  val extra = List(1) ++:
    List(2, 3)
  println(extra)

  // resource handling
  val out = Using.resource(new java.io.StringReader("abc")): reader =>
    reader.read().toChar
  println(out)
ah
ha
ah
ha
List(3.625, 1.5, 2.5)
24.5
List(Pico: long, Lago: short, Cumbre: short)
List(PICO, CUMBRE)
plan done
24.5
List(1, 2, 3)
a

What the example shows:

  • repeat(2): passes the indented block as the by-name argument of the last parameter list. A method such as timed("plan"): with a second parameter list works the same way: the colon applies to the last argument.

  • trails.map: t => followed by an indented body is a lambda with one parameter. The same shape works for foldLeft(0.0): (acc, t) => and for Using.resource(...): reader =>.

  • A colon followed directly by case clauses on the next lines is a partial function (a match without a scrutinee).

  • In a chain, a line that starts with . is accepted even when its indentation does not match any earlier line, as long as it differs by at least two spaces from both neighbouring regions.

  • A colon can also follow an infix operator written in backticks, as in the reference’s credentials example. This was run separately (it is not in the listing above):

//> using scala 3.10.0
@main def run(): Unit =
  val extra = List(1) `++`:
    List(2, 3)
  println(extra)
List(1, 2, 3)

The trailing : of a right-associative operator such as ++: is part of the operator name, so there the colon does not open an argument.

  • A call with parenthesised arguments can end with a colon argument as well: the foldLeft( example above passes "start" in parentheses on its own line and the lambda after ): (acc, x) =>.

The block is always the last argument. A single lambda is the common case, but any parameter, including a by-name one, can be supplied this way. The method itself is unchanged: timed and repeat are ordinary Scala 2-style definitions, and the same code can be written with braces.

The colon argument always ends the call expression, so it can only supply the last argument. It is not confused with a type ascription such as val x: Int, because there the colon is followed by a type on the same line and no indented block.

Mixing braces and indentation

Indentation can be mixed freely with braces { }, brackets [ ] and parentheses ( ). For interpreting indentation in such regions:

  • the assumed indentation width of a multiline region in braces is the indentation of the first token that starts a new line after the opening brace;

  • for a multiline region in brackets or parentheses, it is the indentation of the token following the opening bracket or parenthesis if that is at the end of a line, and the indentation of the enclosing region otherwise;

  • on a closing brace, bracket or parenthesis, as many outdents as necessary are inserted to close all open nested regions.

//> using scala 3.10.0

class Planner {                       // braces still work
  def plan(kms: List[Double]): Double =
    kms.map: km =>                    // indentation inside braces
      km * 1.25
    .sum
}

def grade(km: Double): String = km match
case k if k > 10 => "long"            // cases at the same indent as 'match'
case k if k > 5  => "medium"
case _           => "short"

@main def run(): Unit =
  println(Planner().plan(List(1.0, 2.0)))
  println(grade(12))
  // a leading infix operator continues the previous line
  val total = 1
    + 2
    + 3
  println(total)
  // a call with its arguments on a more-indented line starting with '('
  def add(a: Int)(b: Int) = a + b
  val z = add(1)
    (2)
  println(z)
  // fewer braces inside parentheses
  val s = List(1, 2, 3).foldLeft(
    "start"
  ): (acc, x) =>
    acc + x
  println(s)
3.75
long
6
3
start123

The example uses three further rules from the reference:

  • Case clauses. A match (or catch) may be followed by case clauses at the same indentation as the match itself. The region ends at the first token at that width that is not a case. Most code indents the cases one level; the unindented form is accepted for compatibility.

  • Leading infix operators. A line that starts with an infix operator continues the previous expression, so 1, + 2, + 3 on three lines is 6 (the example prints 6).

  • A dot at an odd indent (shown above) is accepted for method chains.

Indentation and statement continuation

Indentation also decides whether two consecutive lines are one statement. A virtual semicolon is not inserted if the second line is indented more than the first and either starts with (, [ or { or the first line ends with return. In Scala 2 a line starting with { always continues the call on the previous line, and that behaviour is kept under -no-indent.

//> using scala 3.10.0
def add(a: Int)(b: Int): Int = a + b

@main def run(): Unit =
  val more =
    add(1)
      (2)
  println(more)
3

If the second line were aligned with the first ((2) at the same column as add(1)), it would be a separate statement and the compiler would reject the first line:

[error] ./w4.scala:6:5
[error] missing argument list for value of type Int => Int
[error]     add(1)
[error]     ^^^^^^

Tabs and spaces

Indentation prefixes can be spaces, tabs or a mix. Indentation widths are the prefixes themselves, ordered by the string prefix relation: "2 tabs followed by 4 spaces" is strictly less than "2 tabs followed by 5 spaces", but incomparable to "6 tabs" or to "4 spaces followed by 2 tabs". It is an error if the indentation width of a line is incomparable with the width of the current region. Mixing a tab-indented line into a file whose enclosing lines use spaces gives:

[error] ./t1.scala:4:2
[error] Incompatible combinations of tabs and spaces in indentation prefixes.
[error] Previous indent : 2 spaces
[error] Latest indent   : 1 tab
[error] 	val y = x + 1
[error]  ^

A file that uses only tabs compiles and runs. The official style guide uses two spaces and no tabs, which avoids this whole class of error.

A line whose width sits between two earlier widths is a separate, common error. Here the body of f is indented four spaces further than def f, and the result line is indented two:

//> using scala 3.10.0
@main def run(): Unit =
  def f(x: Int) =
      val y = x + 1
    y * 2
  println(f(1))
[error] ./e6.scala:5:5
[error] The start of this line does not match any of the previous indentation widths.
[error] Indentation width of current line : 4 spaces
[error] This falls between previous widths: 2 spaces and 6 spaces
[error]     y * 2
[error]     ^

The same message appears when else is indented one space further than the if it belongs to:

[error] ./e8.scala:12:4
[error] The start of this line does not match any of the previous indentation widths.
[error] Indentation width of current line : 3 spaces
[error] This falls between previous widths: 2 spaces and 4 spaces
[error]    else
[error]    ^

End markers

Indentation has one drawback: it is hard to see where a long region ends, and a closing brace by itself does not say which region it closes either. An end marker is an optional line made of the identifier end and a following specifier, placed at the indentation of the construct it closes. It is ordinary syntax that happens to read like a keyword, and end stays an ordinary identifier everywhere else (the example declares val end = 5).

//> using scala 3.10.0

package trailhead:

  enum Segment:
    case Path(km: Double)
    case Hut(beds: Int)
  end Segment

  class Route(segments: Vector[Segment]):

    def this() =
      this(Vector.empty)
    end this

    def totalKm: Double =
      var sum = 0.0
      var i = 0
      while i < segments.size do
        segments(i) match
          case Segment.Path(km) => sum += km
          case Segment.Hut(_)   => ()
        end match
        i += 1
      end while
      sum
    end totalKm

    def beds: Int =
      val total =
        for
          case Segment.Hut(b) <- segments
        yield b
      end total
      total.sum
    end beds

  end Route

  given Ordering[Route] =
    Ordering.by(_.totalKm)
  end given

  extension (r: Route)
    def isEmpty: Boolean = r.totalKm == 0.0
  end extension

end trailhead

@main def run(): Unit =
  import trailhead.*
  val r = Route(Vector(Segment.Path(3.5), Segment.Hut(12), Segment.Path(1.5)))
  println(r.totalKm)
  println(r.beds)
  println(r.isEmpty)
  println(Route().isEmpty)
  val end = 5          // 'end' is an ordinary identifier elsewhere
  println(end + 1)
5.0
12
false
true
6

The specifier must correspond to the statement that precedes the marker:

Preceding statement Specifier

defines a member x (class, object, trait, enum, method, val, var, type)

the same identifier x

defines a constructor

this

defines an anonymous given

given

defines an anonymous extension

extension

defines an anonymous class

new

a val binding a pattern

val

a package clause for package p

p

an if, while, for, try or match

that same keyword

A wrong specifier or a marker at the wrong column is rejected:

[error] ./w7.scala:4:1
[error] misaligned end marker
[error] end Trail2
[error] ^^^^^^^^^^

The same "misaligned end marker" message appears for end while closing an if, or end g closing f.

When to use them

The reference recommends end markers where the extent of a region is not clear at a glance: when the construct contains blank lines, when it is long (15 to 20 lines or more) or when it ends heavily indented (four levels or more). Otherwise they only add noise. If several nested regions qualify, mark only the outermost; the cascade in the example above is deliberately over-marked to show every kind and is not a style to copy. The marker lines themselves are legal in the package, template and block positions.

Compiler options and rewrites

Option Effect

-indent

allow significant indentation (the default); with -rewrite it turns braces into indented regions

-no-indent

require classical braces; indentation is not significant but is still checked against the braces (warnings above)

-new-syntax

require then and do in control expressions; with -rewrite it drops the parentheses and braces around conditions and generators

-old-syntax

require parentheses around conditions; with -rewrite it inserts them again

-rewrite

rewrite sources in place (combined with one of the options above or with a -migration source version)

Also, -no-indent, -old-syntax and -source 3.0-migration all turn off significant indentation. The option names above are the ones used by the reference; each is also available as a compiler setting in sbt (scalacOptions) and as //> using option in a Scala CLI file.

With significant indentation off, the example from the colon section no longer compiles:

//> using scala 3.10.0
//> using option -no-indent

@main def run(): Unit =
  val x = 1
  if x > 0 then
    println("positive")
[error] ./ni.scala:4:24
[error] expression expected but val found
[error] @main def run(): Unit =
[error]                        ^
[error] ./ni.scala:6:3
[error] Illegal start of toplevel definition
[error]   if x > 0 then
[error]   ^^

Rewriting a Scala 2 style file

The two rewrites cannot be combined in one step: -indent only works on new-style control syntax, so a classical file goes through the compiler twice, first with -rewrite -new-syntax and then with -rewrite -indent. The reverse direction is -rewrite -no-indent, then -rewrite -old-syntax. The rewrite edits the source in place, so run it on a copy or under version control. With Scala CLI the options go in //> using option lines. The starting file:

//> using scala 3.10.0

class Trail(val name: String) {
  def label(km: Double): String = {
    if (km > 10) {
      name + " (long)"
    } else {
      name
    }
  }
}

object Main {
  def main(args: Array[String]): Unit = {
    val t = new Trail("Pico")
    for (k <- List(4.0, 12.0)) {
      println(t.label(k))
    }
  }
}

After one run with //> using option -rewrite -new-syntax the control syntax changed but the braces remain:

class Trail(val name: String) {
  def label(km: Double): String = {
    if km > 10 then {
      name + " (long)"
    } else {
      name
    }
  }
}

object Main {
  def main(args: Array[String]): Unit = {
    val t = new Trail("Pico")
    for k <- List(4.0, 12.0) do {
      println(t.label(k))
    }
  }
}

Changing the option line to -rewrite -indent and running again produced the braceless program, and the program printed Pico and Pico (long) both before and after:

class Trail(val name: String):
  def label(km: Double): String =
    if km > 10 then
      name + " (long)"
    else
      name

object Main:
  def main(args: Array[String]): Unit =
    val t = new Trail("Pico")
    for k <- List(4.0, 12.0) do
      println(t.label(k))
[patched file .../rw/step1.scala]
Pico
Pico (long)

Going back, -rewrite -no-indent turned the braceless file into braces for classes and methods but left the if/for bodies in new syntax (they had no indentation block to convert); a second pass with -rewrite -old-syntax then wrote the conditions as if (km > 10) and for (k <- List(4.0, 12.0)). The Scala 3 migration guide shows the same two-step flow on a larger example.

Formatting with scalafmt

Scalafmt (3.11.5 at the check date) formats Scala 3 code once runner.dialect is a Scala 3 dialect in .scalafmt.conf, and it can apply the same rewrites as the compiler:

version = 3.11.5
runner.dialect = scala3
rewrite.scala3.convertToNewSyntax = true
rewrite.scala3.optionalBraces.enabled = true

convertToNewSyntax rewrites if (c), while (c) and for (g) to the quiet form, and also (depending on further flags) : _* to * and _ imports to *. The rewrite.scala3.optionalBraces section, available since scalafmt 3.8.1, removes optional braces and applies significant indentation; its sibling rewrite.scala3.endMarker controls inserting and removing end markers. Older scalafmt releases called the first setting rewrite.scala3.removeOptionalBraces, so check the version in your build against the configuration page before relying on a name.

Relaxed lambda syntax (SIP-75, preview in 3.10)

Relaxed lambda syntax is a package of small improvements that make function literals after a colon more flexible. It is a preview feature in Scala 3.10.0: it is switched on with the -preview compiler flag and its details may still change before it becomes standard. It was experimental in 3.9, so it is not available as a stable feature on the 3.9 LTS line (or the 3.3 LTS). Related, SIP-74 (parenthesised partial function literals) is also a preview in 3.10. The enabling line in a Scala CLI file is //> using option -preview; in sbt, add -preview to scalacOptions. These examples were run with Scala 3.10.0.

//> using scala 3.10.0
//> using option -preview

case class Trail(name: String, km: Double)

def fun(f: Int => Int => Int): Int = f(1)(2)

@main def run(): Unit =
  val trails = List(Trail("Pico", 12.5), Trail("Lago", 4.0))
  val names = trails.map: t => t.name.toUpperCase
  println(names)
  val pairs = List((1, 2), (3, 4))
  val sums = pairs.map: (a, b) => a + b
  println(sums)
  val firsts = pairs.collect: case (a, b) if b > 2 => a
  println(firsts)
  val inParens = pairs.collect(case (a, b) if b > 2 => a)
  println(inParens)
  val curried = fun: (x: Int) => y =>
    x + y
  println(curried)
  val partial: PartialFunction[(Int, Int), Int] = case (a, b) if b > 2 => a
  println(partial.isDefinedAt((1, 1)))
  val nested = trails.map: t => t.name + trails.count(u => u.km > t.km)
  println(nested)
List(PICO, LAGO)
List(3, 7)
List(3)
List(3)
3
false
List(Pico0, Lago1)

The three parts are:

  • Single-line lambdas. A lambda that follows a colon on the same line, xs.map: x => x + 1, was not accepted before (the arrow had to be followed by a newline and an indented block). The lambda extends to the end of the line. It works with one or more parameters, with type parameters, and with a single case. It does not work for function values without an arrow: xs.map: _ + _ and xs.map: plus1 are errors. The new forms do not apply to code that is immediately enclosed in parentheses, because (x: Int => Int) there is a type ascription.

  • Curried multi-line lambdas. Several parameter sections, each followed by an arrow, can open one indented block: fun: (x: Int) => y => as in the example.

  • Case expressions. A single case clause can be written inside parentheses or as an expression, as in the collect(case ...) call and the PartialFunction value above. SIP-74 specifies this part.

Without -preview the same code is rejected on 3.10.0, with messages that do not mention the missing flag:

//> using scala 3.10.0
@main def run(): Unit =
  val xs = List(1, 2)
  val ys = xs.map: x => x + 1
  println(ys)
[error] ./e4.scala:4:12
[error] not a legal formal parameter for a function literal
[error]   val ys = xs.map: x => x + 1
[error]            ^^^^^^^^^
[error] ./e4.scala:4:25
[error] Not found: x
[error]   val ys = xs.map: x => x + 1
[error]                         ^

The reference text says single-line lambdas can nest (xs.map: x => x.toString + xs.dropWhile: y => y > 0), but Scala 3.10.0 rejects exactly that line and asks for parentheses around the second lambda:

[error] ./n.scala:5:50
[error] Can't have two single line lambdas on the same line, enclose the second in parentheses
[error]   val ys = xs.map: x => x.toString + xs.dropWhile: y => y > 0
[error]                                                  ^

The example above uses trails.count(u => ...) for that reason. Preview features are described on Language Evolution: SIPs, Preview & Experimental, and function values in general on Functions as Values.

Comparing with other languages

Language Block structure Scala 3 counterpart

Scala 2

braces only; if (c), for (x <- xs); a line starting with { always continues the previous call

the same code compiles unchanged; braces and parentheses stay legal

Java

braces; if (c) { ... }; indentation is cosmetic

if c then, while c do, or the same braced code

Kotlin

braces; if (c) ...; trailing lambda xs.map { x -> ... }

trailing lambda with xs.map: x =>

Python

indentation; a colon ends every compound statement header

a colon only where a template body or an argument starts; then/do/=/else etc. open regions without a colon

The trailing-lambda feel is the closest parallel to Kotlin (xs.map { x -> ... }) and Groovy closures. For comparison see Java lambdas and method references and Java control flow. Python’s mandatory indentation is described on Python lexical structure and style. Unlike Python, Scala’s indentation is optional (braces always work) and a mismatch is a compile-time warning or error, not a different program.

Pitfalls

  • Mixing tabs and spaces. The error shown above; configure the editor to insert spaces.

  • Forgetting then or do. if x > 0 println(...) gives "'then' expected".

  • A line that looks aligned but is not. A line indented between two earlier widths is rejected ("does not match any of the previous indentation widths").

  • Aligned versus indented continuation. f(x) followed by (y) at the same column is two statements; indented, it is a single call.

  • Colons. A trailing colon after a call opens an argument and a trailing colon after a class, trait, object or enum header opens its body; a colon after any other statement is a syntax error.

  • Misaligned end marker. The marker must be on its own line at the column of the construct it closes and name it exactly.

  • -no-indent leftovers. Code written braceless fails to compile under -no-indent; old code stays valid under indentation.

  • Preview flag. Relaxed lambdas need -preview on 3.10; the errors without it do not mention the flag.

Where the book is outdated

Books written for Scala 2 show only braces and parentheses; they do not cover then/do, a colon for template bodies, end markers or fewer braces, all of which are valid Scala 3 and sometimes the form used by newer documentation. Early Scala 3 material (2021 and 2022) describes "fewer braces" as experimental, requiring import language.experimental.fewerBraces; the import is unnecessary since the feature became stable in 3.3. The given ... with form for instances and the : _* vararg splice are older spellings that newer code replaces with a trailing colon and *.