Conditionals & Loops

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, 2026-10-12) and every example was run with Scala CLI on Scala 3.10.0 and JDK 26 (checked on 2026-10-15). Everything here also works on the 3.9 LTS line and on 3.3 LTS: the quiet syntax, end markers and the dropping of do/while all arrived with Scala 3.0, and no feature on this page needs a -preview or experimental flag.

Two ideas shape this page. First, if is an expression: it has a value, so it replaces the ternary operator that Java, Kotlin (also an expression) and JavaScript need. Second, loops are statements built on a few primitives. while is the only loop keyword apart from for, do/while no longer exists, and there is no break or continue. The for loops and comprehensions and match expressions have their own pages, and early exit from a loop is covered in Exceptions, boundary & Early Exit.

The examples use the trailhead scenario: a hiker’s app that reads soil moisture and trail sensor readings.

if / then / else as an expression

The Scala 3 form of a conditional is if condition then thenBranch else elseBranch. The then keyword replaces the parentheses around the condition, which is what the compiler needs to know where the condition ends when the braces are optional (see Optional Braces & Indentation). The whole construct is an expression, so its value can be bound, returned or passed on. The branches can be on one line or indented under then and else.

//> using scala 3.10.0

@main def ifExpr(): Unit =
  val moisture = 42
  val advice = if moisture < 20 then "water now" else if moisture < 60 then "ok" else "too wet"
  println(advice)

  val level =
    if moisture < 20 then
      "low"
    else if moisture < 60 then
      "medium"
    else
      "high"
  println(level)
ok
medium

The type of the expression is the least upper bound of the branch types, and with different branch types the compiler infers a union type or a common supertype rather than failing. A block in a branch has the value of its last expression, so no return is needed. Other layouts are accepted too: then may start its own line, a branch body may begin on the same line as then and continue indented, and a single-line if can carry both branches.

//> using scala 3.10.0
@main def layouts(): Unit =
  val x = 5
  val a = if x > 3 then "a" else "b"; println(a)
  if x > 3 then println("one-liner") else println("no")
  if x > 3
  then println("then on its own line")
  val b = if x > 3 then
    "indented body"
  else "other"
  println(b)
  val t: Int | String = if x > 3 then 1 else "none"
  println(t)
a
one-liner
then on its own line
indented body
1

if without else

An if with no else is still an expression, and the missing branch is the unit value (). The result type is therefore Unit, whatever the then-branch’s type, so an if without else is meant for side effects. When the then-branch produces a non-Unit value and the result is bound, the compiler tells you the value is thrown away. Running the example below shows the warning that Scala 3.10.0 prints by default, without any extra flag.

//> using scala 3.10.0

@main def noElse(): Unit =
  val moisture = 42
  val result = if moisture > 100 then "flood"
  println(result)
  println(result.getClass)
[warn] ./noElse.scala:5:39
[warn] Discarded non-Unit value of type String. Add `: Unit` to discard silently.
[warn]   val result = if moisture > 100 then "flood"
[warn]                                       ^^^^^^^
()
void

result is () and its runtime class prints as void, the JVM name for the unit value’s class. Give the if an else, or make the branch a Unit expression such as println(...), when a value is wanted.

end markers on conditionals

Long conditionals in indentation-based code can be closed with an end if marker. The marker is optional, is checked by the compiler (an end that does not match the construct it closes is an error) and is used by convention when the body spans many lines. It carries no meaning beyond readability. The same markers exist for while, for, match, try, classes and methods.

//> using scala 3.10.0

def check(n: Int): String =
  if n < 0 then
    "negative"
  else
    "non-negative"
  end if

@main def endMarker(): Unit =
  println(check(-3))
negative

while loops

while condition do body repeats the body while the condition is true. The condition is re-evaluated before each iteration, so the body may run zero times. The loop is an expression of type Unit: its value is always (). Because Scala has no break and no continue, a loop exits through its condition. A common idiom is a var flag that the body updates, or a condition that calls a side-effecting method such as an iterator’s hasNext.

//> using scala 3.10.0

@main def whileLoops(): Unit =
  var i = 0
  while i < 3 do
    println(s"tick $i")
    i += 1
  end while

  val readings = Iterator(3, 7, 0, 9)
  var total = 0
  var done = false
  while !done do
    val r = readings.next()
    if r == 0 then done = true
    else total += r
  println(total)

  val res = while false do ()
  println(res)
tick 0
tick 1
tick 2
10
()

while is imperative: it needs a var, which the style guide and the functional parts of the language discourage. Most Scala code instead uses foreach, for, folds or recursion on collections (see Recursion & Tail Calls) and keeps while for performance-critical inner loops and for reading from sources such as iterators. for x <- 1 to 3 do ... is the loop form for iterating a range or collection, and it takes the same do keyword.

do / while is dropped

The Java-style do body while condition loop was removed from the language in Scala 3.0. The reference gives the reason: it is rarely used and is easily replaced by a while whose condition contains the body. Compiling the old form shows a parse error.

//> using scala 3.10.0

@main def old(): Unit =
  var n = 0
  do
    n += 1
  while n < 3
  println(n)
[error] ./old.scala:5:3
[error] end of toplevel definition expected but 'do' found
[error]   do
[error]   ^^
Error compiling project (Scala 3.10.0, JVM (26))

The replacement is a while loop whose condition is a block. The block runs the body first and ends with the test, and the loop body is the empty () (this is the "loop and a half" shape):

//> using scala 3.10.0

@main def doWhile(): Unit =
  var n = 10
  while
    n += 1
    println(s"n=$n")
    n < 12
  do ()
n=11
n=12

The condition block runs at least once, like a do/while body does, and the last expression, n < 12, decides whether to go round again. When the test has to sit in the middle of the body, put the first half of the body in the condition block and the second half after do. The same idea expresses "read, then stop at a sentinel".

The Unit result and value discard

Unit has a single value, (), and it is the type of statements: assignments, while loops, an if without else and methods declared as : Unit. A method that is declared to return Unit accepts any expression as its last line, because the compiler inserts a conversion that evaluates the value and discards it. This is called value discarding, and it can hide a mistake: calling a method that returns a useful result and forgetting to use it.

Two compiler options make this visible. -Wvalue-discard warns when a non-Unit value is discarded to conform to a Unit expected type, such as the last line of a Unit method. -Wnonunit-statement warns about a non-Unit expression in statement position, that is, a value computed in the middle of a block and ignored. They cover different positions, so use both to catch both mistakes. Passing : Unit as a type ascription (as in the message "Add : Unit to discard silently") marks the discard as intentional.

//> using scala 3.10.0
//> using option -Wvalue-discard
//> using option -Wnonunit-statement

def log(msg: String): Int =
  println(msg)
  msg.length

def sideEffect(): Unit =
  log("hello")
  ()

def discard(): Unit =
  log("x")

@main def run(): Unit =
  sideEffect()
  discard()
[warn] ./warnings.scala:10:3
[warn] unused value of type Int
[warn]   log("hello")
[warn]   ^^^^^^^^^^^^
[warn] ./warnings.scala:14:3
[warn] discarded non-Unit value of type Int. Add `: Unit` to discard silently.
[warn]   log("x")
[warn]   ^^^^^^^^
hello
x

Running the same file with only one of the two flags produced only that flag’s warning: -Wnonunit-statement alone reported the "unused value of type Int" at line 10, and -Wvalue-discard alone reported the discarded value at line 14. The //> using option lines are Scala CLI directives; with sbt the same flags go into scalacOptions. The full list of warning options is in Compiler Options, Linting & Errors.

Quiet syntax and the Scala 2 forms

The parentheses around conditions are not required in Scala 3, but they are not forbidden either. The compiler still accepts the Scala 2 shapes, if (cond) a else b, while (cond) { ... } and for (x <- xs) ..., so code migrates without rewriting. The Scala 3 "quiet" forms use then and do instead.

//> using scala 3.10.0

@main def classic(): Unit =
  var x = 3
  while (x > 0) { x -= 1 }
  for (i <- 1 to 2) println(i)
  for i <- 1 to 2 do println(i)
  while x < 2 do x += 1
  println(x)
  val s = if (x > 1) "y" else "n"
  println(s)
1
2
1
2
2
y

Three compiler options control how strictly the syntax is enforced and can also rewrite code:

  • -new-syntax rejects the parenthesised if and while forms. With the example above it reported "This construct is not allowed under -new-syntax or -language:future" for the while (x > 0) and if (x > 1) lines, and added that the code can be rewritten automatically with -rewrite -source 3.0-migration -new-syntax.

  • -old-syntax does the opposite and rejects the quiet forms. Running the first example of this page with -old-syntax reported "This construct is not allowed under -old-syntax" at its if ... then lines.

  • -rewrite combined with -source 3.0-migration and one of the two syntax flags converts a code base automatically. The migration guide linked below describes the procedure, and it is good practice to commit before running a rewrite.

A missing then is a parse error

Writing if x > 3 println("a") without then is a common slip when moving from Java. The compiler stops at the end of the line, because it cannot tell where the condition ends:

//> using scala 3.10.0

@main def bad(): Unit =
  val x = 5
  val y = if x > 3 println("a")
[error] ./bad.scala:6:1
[error] 'then' expected, but eof found
[error] ./bad.scala:5:14
[error] value println is not a member of Boolean
[error]   val y = if x > 3 println("a")
[error]              ^^^^^^^^^^^^^
Error compiling project (Scala 3.10.0, JVM (26))

The second message is the more confusing one: without then the compiler reads 3 println("a") as an infix call.

Comparison with other languages

  • Java: if is a statement and the condition needs parentheses. The ternary cond ? a : b is an expression, but nested ternaries are hard to read. Scala has no ternary operator because if is already an expression. Java has do/while, break and continue, which Scala lacks. See Java Control Flow.

  • Kotlin: if is also an expression (val a = if (c) 1 else 2), but conditions keep their parentheses and do ... while exists, along with break and continue with labels. See Kotlin Control Flow.

  • Groovy: the Groovy truth lets any object be a condition. Scala conditions must be Boolean, with no truthiness for null, 0 or an empty string. See Groovy Control Structures.

  • Scala 2: if (cond) a else b and while (cond) body are the only forms, and do body while (cond) exists. Scala 2 code compiles on Scala 3 except for do/while.

The Boolean condition requirement catches the common bug of treating an Option or a String as a condition. A condition of the wrong type is a compile error, not a silent branch.

Where the books are outdated

Books written for Scala 2 teach if (cond) with parentheses as the only form, include the do/while loop, use braces for every block, and rarely mention end markers, the quiet then/do syntax or the -Wvalue-discard options. Their advice on Unit-returning methods also predates the Scala 3 warning for an if without else. In Scala 3 the if is an expression in the same way it was in Scala 2, so the semantics are unchanged, but the preferred form is then, and do/while must be rewritten as a while with a block condition.