Free Handbook · Every example compiled & verified

Expressions & Control

In Scala if, match, blocks and for all return values. Learn control flow as expressions, while loops, for-comprehensions with yield, and optional braces.

0 / 142 lessons🔥 0 day streak
ShareXLinkedIn

Module 02 · what you'll be able to do

  • Use if/else, blocks and match as expressions that produce a value instead of assigning inside branches
  • Explain Unit and predict when an expression quietly evaluates to ()
  • Write while loops and stop a loop early with boundary and break
  • Build collections with for-comprehensions using ranges, guards, several generators and yield
  • Read and write both Scala 3 indentation syntax and the braces style found in Scala 2 and Spark code
01

Everything is an expression

In Java or C, if is a statement: it does something but has no value, so you declare a variable first and assign it inside each branch. In Scala, if is an expression: it evaluates to the value of whichever branch ran, and you can put that value straight into a val. The same is true of blocks, match, try and for ... yield. This one idea is why Scala code needs so few vars.

scalaMain.scala
@main def run(): Unit =
  val temp = 31
  val feel = if temp > 30 then "hot" else "fine"
  println(feel)

  val bmi =
    val weightKg = 70.0
    val heightM = 1.75
    weightKg / (heightM * heightM)
  println(f"$bmi%.1f")

  println(if temp % 2 == 0 then "even" else "odd")
Outputcompiled & run with real Scala
hot
22.9
odd

The indented lines under val bmi = are a block. A block evaluates to its last expression; weightKg and heightM exist only inside it.

Your turn

Add a third branch with else if temp < 15 then "cold" and change temp to 10.

Statement style (Java habit)

  • var label = "" declared first
  • if (n > 0) label = "pos" else label = "neg"
  • The compiler cannot check every branch assigned it
  • Needs a var

Expression style (Scala)

  • val label = if n > 0 then "pos" else "neg"
  • One line, one name, assigned once
  • The compiler checks the branch types
  • Stays a val
No ternary operator
Scala has no cond ? a : b. It does not need one: if cond then a else b already is the ternary. The return keyword exists too, but idiomatic Scala almost never uses it — the last expression of a method is its result.
Error you will hit

if without else has no value to give

scala
@main def run(): Unit =
  val stock = 0
  val label: String = if stock > 0 then "in stock"
  println(label)
-- [E007] Type Mismatch Error: Main.scala:3:22
3 |  val label: String = if stock > 0 then "in stock"
  |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |                   Found:    Unit
  |                   Required: String
  |
  |                   Maybe you are missing an else part for the conditional?
1 error found
Compilation failed
Why the compiler said that

When stock is 0 the if has nothing to return, so an if without else always has type Unit. A Unit cannot go into a String. The compiler even guesses the cause for you.

The fix

Give every expression-style if an else, so there is a value on both paths.

scala
@main def run(): Unit =
  val stock = 0
  val label: String = if stock > 0 then "in stock" else "sold out"
  println(label)
02

if / else if / else

Scala 3 writes a condition as if cond then a else b. The older form with parentheses, if (cond) a else b, still compiles and is what you will see in Scala 2 and Spark code — both mean exactly the same thing. Chain more cases with else if. Conditions must be a real Boolean: Scala has no "truthy" values, so if count then or if name then will not compile.

scalaMain.scala
@main def run(): Unit =
  val score = 78
  val grade =
    if score >= 90 then "A"
    else if score >= 75 then "B"
    else if score >= 60 then "C"
    else "F"
  println(s"$score -> $grade")

  val old = if (score > 50) "pass" else "fail"
  println(old)

  if score > 70 then
    println("well done")
    println("certificate unlocked")
Outputcompiled & run with real Scala
78 -> B
pass
well done
certificate unlocked

The last if has no else: it is used only for its side effects (printing), so there is no value to lose. A multi-line branch is simply indented under then.

Your turn

Turn the grade into a match with guards once you have read the next lesson but one — which version reads better?

If the branches return different types, the compiler picks the closest common type. if ok then 1 else 2.5 is a Double (the Int widens). if ok then 1 else "one" compiles too, but the result is a type so general you can do almost nothing with it — a hint that the two branches should really return the same type.

03

Unit and side effects

Unit is the type of an expression that is evaluated only for what it does — print, write a file, change a var — not for a result. It has exactly one value, written (). println returns Unit, an assignment returns Unit, and a for loop with do returns Unit. It plays the role of Java's void, with one difference: Unit is a real value, so it can be stored and printed.

scalaMain.scala
@main def run(): Unit =
  var n = 0
  val a = println("side effect")
  val b = { n = 5 }
  val c = for i <- 1 to 3 do n += i
  println(a)
  println(b)
  println(c)
  println(n)
  println(a == ())
Outputcompiled & run with real Scala
side effect
()
()
()
11
true

All three vals hold (). The work happened (the text printed, n became 5 and then 11), but the expressions themselves produced nothing useful.

The classic for-loop surprise
Writing val doubled = for n <- 1 to 3 do n * 2 compiles and gives you (), not a list. do means "run this for each item and throw the result away". To collect the results you need yield — see for-comprehensions below.

That is also why a method written for its effect declares : Unit — like @main def run(): Unit in every example here. If you write : Unit and the body ends in a String, the compiler keeps the side effects and throws the string away; since Scala 3 it warns you when that looks accidental.

04

match as an expression

match is Scala's switch, but far more capable and — like if — an expression. The value on the left is compared against each case from top to bottom; the first one that fits wins and its right-hand side becomes the result. There is no fall-through and no break. _ is the "anything else" case, and | lets one case accept several values.

scalaMain.scala
@main def run(): Unit =
  val day = 6
  val kind = day match
    case 6 | 7 => "weekend"
    case 1     => "Monday"
    case _     => "weekday"
  println(kind)

  val temp = -4
  val band = temp match
    case t if t < 0  => "freezing"
    case t if t < 20 => "mild"
    case _           => "warm"
  println(band)

  for text <- List("42", "4x2") do
    text.toIntOption match
      case Some(n) => println(s"number $n")
      case None    => println(s"'$text' is not a number")
Outputcompiled & run with real Scala
weekend
freezing
number 42
'4x2' is not a number

case t if t < 0 binds the value to t and adds a guard. Some(n) takes the number out of the Option that toIntOption returned — the clean way to handle bad input promised in Module 01.

Your turn

Add a case for 0 exactly, printing "zero", in the right place so it wins over the t < 20 guard.

This is only the start
match can also take apart tuples, lists, case classes and your own types, and the compiler can check you covered every case. That is a whole module: Module 05 — Case Classes & Pattern Matching.
05

while loops and breaking out

while cond do body repeats the body as long as the condition is true. It returns Unit and needs a var to make progress, which is why idiomatic Scala uses it less than Java does — collection methods (Module 07) usually say the same thing more clearly. It is still the right tool for "keep going until something happens" loops like reading input or retrying.

scalaMain.scala
@main def run(): Unit =
  var n = 9471
  var digitSum = 0
  while n > 0 do
    digitSum += n % 10
    n /= 10
  println(digitSum)

  var attempts = 0
  while
    attempts += 1
    attempts < 3
  do ()
  println(s"stopped after $attempts attempts")
Outputcompiled & run with real Scala
21
stopped after 3 attempts

Scala 3 dropped do { ... } while (cond). The second loop is the replacement: the body goes in the condition block, whose last expression is the test, and do () is an empty body. It always runs at least once.

There is no break or continue keyword. To stop early, wrap the loop in scala.util.boundary and call break(value): the boundary block immediately ends with that value. If the loop finishes normally, the block's last expression is the result.

scalaMain.scala
import scala.util.boundary, boundary.break

@main def run(): Unit =
  val readings = List(3, 8, -2, 5, -9)
  val firstNegative = boundary:
    for r <- readings do
      if r < 0 then break(r)
    0
  println(firstNegative)

  for r <- readings do
    if r >= 0 then
      println(s"ok $r")
Outputcompiled & run with real Scala
-2
ok 3
ok 8
ok 5

The second loop is how you write "continue": put the work inside an if and skip the items you do not want. In real code, readings.find(_ < 0) does the first job in one call.

Error you will hit

'do' expected — a while loop without do

scala
@main def run(): Unit =
  var i = 0
  while i < 3
    println(i)
    i += 1
-- [E040] Syntax Error: Main.scala:5:9
5 |    i += 1
  |         ^
  |         'do' expected, but integer literal found
-- [E008] Not Found Error: Main.scala:4:4
3 |  while i < 3
4 |    println(i)
  |        ^
  |        value println is not a member of Boolean.
  |        Note that `println` is treated as an infix operator in Scala 3.
  |        If you do not want that, insert a `;` or empty line in front
  |        or drop any spaces behind the operator.
2 errors found
Compilation failed
Why the compiler said that

Without parentheses around the condition, Scala 3 needs do to know where the condition ends and the body begins. With no do, it read the indented lines as a continuation of the condition — i < 3 println(i) ... — which is nonsense, so you get two confusing errors. When a syntax error points at odd places, look at the line just above.

The fix

Add do after the condition (or write the Scala 2 form while (i < 3) { ... }).

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

for-comprehensions, ranges, guards and yield

A for in Scala walks over anything iterable: a range, a list, a string, an Option. 1 to 5 is a range that includes 5; 1 until 5 stops before it; by sets the step. Add if after a generator to filter (a guard), and several generators to nest loops. End with do to run code for each item, or yield to build a new collection from the results.

scalaMain.scala
@main def run(): Unit =
  for i <- 1 to 3 do println(s"tick $i")
  println((1 until 4).toList)
  println((10 to 0 by -5).toList)

  val evenSquares = for n <- 1 to 10 if n % 2 == 0 yield n * n
  println(evenSquares)

  val seats = for
    row <- 1 to 2
    col <- List("A", "B")
  yield s"$row$col"
  println(seats)

  val shout = for c <- "hey" yield c.toUpper
  println(shout)
Outputcompiled & run with real Scala
tick 1
tick 2
tick 3
List(1, 2, 3)
List(10, 5, 0)
Vector(4, 16, 36, 64, 100)
Vector(1A, 1B, 2A, 2B)
HEY

The collection you get back matches what you looped over: a range yields a Vector, a list yields a List, a string yields a String. With two generators the second one runs fully for each value of the first.

Your turn

Print the multiplication table for 3 (3 x 1 = 3 up to 3 x 10 = 30) with a single for.

VisualizeA for with a guard and yieldStep 1 / 9
val result = for
n <- 1 to 4
if n % 2 == 1
yield n * 10
println(result)
Line 2

The generator takes the first value from the range 1 to 4.

Variables now
n1
result(building)
All 9 steps as a table
StepLineWhat happenedVariables now
12The generator takes the first value from the range 1 to 4.n = 1 result = (building)
23Guard: 1 % 2 == 1 is true, so this value continues.n = 1
34yield produces 1 * 10 and adds it to the new collection.n = 1 result = (building) 10
42Next value from the range.n = 2
53Guard: 2 % 2 == 1 is false — 2 is skipped, nothing is yielded.n = 2
62Next value.n = 3
74The guard passes; 30 is yielded.n = 3 result = (building) 10, 30
83n = 4 fails the guard. The range is exhausted, so the for-expression is done.n = 4 result = Vector(10, 30)
95The finished collection is printed.result = Vector(10, 30)
What for really is
The compiler rewrites every for into method calls: for x <- xs yield f(x) becomes xs.map(x => f(x)), a guard becomes withFilter, and extra generators become flatMap. That is why for works on Option, Future and Spark Datasets alike — anything with map and flatMap. Module 07 and Module 08 lean on this heavily.
07

Indentation syntax vs braces

Scala 3 lets you drop braces and use indentation to mark blocks, like Python. Every example in this handbook uses that style. But Scala 3 still accepts the braces style completely, and most existing Scala code — including Spark jobs, which run on Scala 2.12 and 2.13 — is written with braces. You need to read both. The two programs below compile to exactly the same thing.

scalaMain.scala
@main def run(): Unit =
  val nums = List(4, 11, 7)
  for n <- nums do
    if n > 5 then
      println(s"$n is big")
    else
      println(s"$n is small")
  end for
  println("done")
end run
Outputcompiled & run with real Scala
4 is small
11 is big
7 is big
done

Scala 3 style: then and do instead of parentheses, blocks by indentation. The optional end for / end run markers help when a block is long enough that you lose track of where it ends.

scalaMain.scala
object Main {
  def main(args: Array[String]): Unit = {
    val nums = List(4, 11, 7)
    for (n <- nums) {
      if (n > 5) {
        println(s"$n is big")
      } else {
        println(s"$n is small")
      }
    }
    println("done")
  }
}
Outputcompiled & run with real Scala
4 is small
11 is big
7 is big
done

Scala 2 style, still valid Scala 3: parentheses around conditions and generators, braces around every block, and an object with a main method as the entry point.

Scala 3 indentationBraces style
if x > 0 then a else bif (x > 0) a else b
while i < n do ...while (i < n) { ... }
for x <- xs do ...for (x <- xs) { ... }
for x <- xs yield f(x)for (x <- xs) yield f(x)
x match + indented casesx match { case ... }
class A: + indented bodyclass A { ... }
Pick one per file
Mixing styles inside one file compiles but reads badly. Follow the project: a new Scala 3 service can use indentation; a Spark job (see Spark with Scala) should match the braces style its codebase already uses. scalac -rewrite -indent and -rewrite -no-indent convert a file either way automatically.
08

Interpolation with expressions

Module 01 introduced the s, f and raw interpolators. Because everything is an expression, anything can go inside ${ ... } — arithmetic, method calls, even an if or a match. Keep it short, though: if the expression needs more than a glance, give it a val with a good name first.

scalaMain.scala
@main def run(): Unit =
  val items = List("pen", "book", "lamp")
  val total = 1234.5
  println(s"${items.size} item${if items.size == 1 then "" else "s"}")
  println(s"first: ${items.head.toUpperCase}, last: ${items.last}")
  println(f"total: $total%,.2f")
  println(f"${items.size}%03d | ${total / items.size}%8.2f |")
  println(raw"C:\new\table")
  println(s"price: $$${total.toInt}")
Outputcompiled & run with real Scala
3 items
first: PEN, last: lamp
total: 1,234.50
003 |   411.50 |
C:\new\table
price: $1234

%,.2f adds thousands separators; %03d pads with zeros; $$ prints a literal dollar sign. raw kept \n and \t as plain backslash text instead of a newline and a tab.

Error you will hit

f interpolator: the format does not match the type

scala
@main def run(): Unit =
  val name = "Asha"
  val score = 91.5
  println(f"$name%s scored $score%d points")
-- [E209] Interpolation Error: Main.scala:4:28
4 |  println(f"$name%s scored $score%d points")
  |                            ^^^^^
  |         Found: (score : Double), Required: Int, Long, Byte, Short, BigInt
1 error found
Compilation failed
Why the compiler said that

%d means "whole number", but score is a Double. Unlike Java's String.format, which fails at runtime, the f interpolator checks every format against its value's type at compile time.

The fix

Use %f (with a precision such as %.1f) for decimals, or convert the value first with score.toInt if you really want a whole number.

scala
@main def run(): Unit =
  val name = "Asha"
  val score = 91.5
  println(f"$name%s scored $score%.1f points")
Expression
Code that evaluates to a value. In Scala, if, match, blocks, try and for-yield are all expressions.
Block
A group of expressions (indented or in braces) whose value is the value of its last expression.
Unit / ()
The type and single value of an expression evaluated only for its side effect.
Guard
An if inside a for or a case that filters which values continue.
Generator
The x <- xs part of a for, which takes each value from a collection in turn.
yield
Ends a for-comprehension and collects each result into a new collection.
Range
A lazy sequence of numbers: 1 to 5 includes 5, 1 until 5 does not, by sets the step.
boundary / break
scala.util.boundary marks a block that break(value) can exit early with a result.
Optional braces
Scala 3 syntax that uses indentation, then and do instead of braces and parentheses.
Quick check

What does val xs = for n <- 1 to 3 do n * 2 put in xs?

Quick check

Which line compiles in Scala 3?

Frequently asked questions

Does Scala have a ternary operator?
No, because it does not need one. if is an expression, so val max = if a > b then a else b (or if (a > b) a else b in Scala 2 style) is the ternary.
How do I break out of a loop in Scala?
Scala has no break keyword. In Scala 3 wrap the loop in scala.util.boundary and call break(value) to leave it early with a result. Often a collection method is clearer: find, exists, takeWhile or indexWhere stop at the first match on their own.
Should I use braces or indentation in Scala 3?
Both are fully supported. New Scala 3 projects increasingly use indentation; Scala 2 codebases and Spark jobs use braces. Follow the style of the project you are in, and stay consistent within a file.

Finish the Scala handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.