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-syntaxrejects the parenthesisedifandwhileforms. With the example above it reported "This construct is not allowed under -new-syntax or -language:future" for thewhile (x > 0)andif (x > 1)lines, and added that the code can be rewritten automatically with-rewrite -source 3.0-migration -new-syntax. -
-old-syntaxdoes the opposite and rejects the quiet forms. Running the first example of this page with-old-syntaxreported "This construct is not allowed under -old-syntax" at itsif ... thenlines. -
-rewritecombined with-source 3.0-migrationand 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:
ifis a statement and the condition needs parentheses. The ternarycond ? a : bis an expression, but nested ternaries are hard to read. Scala has no ternary operator becauseifis already an expression. Java hasdo/while,breakandcontinue, which Scala lacks. See Java Control Flow. -
Kotlin:
ifis also an expression (val a = if (c) 1 else 2), but conditions keep their parentheses anddo ... whileexists, along withbreakandcontinuewith labels. See Kotlin Control Flow. -
Groovy: the Groovy truth lets any object be a condition. Scala conditions must be
Boolean, with no truthiness fornull,0or an empty string. See Groovy Control Structures. -
Scala 2:
if (cond) a else bandwhile (cond) bodyare the only forms, anddo body while (cond)exists. Scala 2 code compiles on Scala 3 except fordo/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.