Intermediate

I/O Operations in Scala

Learn console input, formatted output, and file reading and writing in Scala with Source, Using, and Try for safe resource handling - with Docker-ready examples

Input and output is where Scala’s dual heritage is most visible. As a JVM language, Scala has the entire java.io and java.nio machinery available—every PrintWriter and FileWriter trick you know from Java works unchanged. But Scala layers its own idioms on top: scala.io.Source turns a file into an iterator of characters or lines so the collection methods you already know (mkString, getLines, zipWithIndex) work on files too, and string interpolators make formatted output both concise and type-checked.

The functional side of Scala shows up in how errors and resources are handled. Instead of scattering try/catch blocks, idiomatic Scala wraps a fallible read in Try, turning an exception into an ordinary value you can pattern match on—a Success carrying the result or a Failure carrying the error. And instead of hand-written finally blocks to close files, scala.util.Using guarantees a resource is released no matter how the block exits.

In this tutorial you’ll go beyond the println you met in Hello World: formatted output with the s and f interpolators, reading from standard input with scala.io.StdIn, and writing, appending to, and reading files—including how to handle a file that doesn’t exist without crashing.

Formatted Output

Scala offers three levels of output formatting. Plain print and println handle simple cases. The s interpolator embeds any expression inside a string with $name or ${expr}. The f interpolator adds printf-style format specifiers—and because it’s checked at compile time, passing a String to a %d slot is a compile error, not a runtime surprise. Classic printf is also available for those who want the Java-style API directly.

Create a file named FormattedOutput.scala:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
object FormattedOutput {
  def main(args: Array[String]): Unit = {
    // print omits the newline; println adds one
    print("Loading")
    print("...")
    println("done")

    // The s-interpolator embeds expressions directly in strings
    val language = "Scala"
    val year = 2004
    println(s"$language first appeared in $year")

    // The f-interpolator is a type-checked printf: bad formats fail at compile time
    val pi = 3.14159265
    println(f"pi to 2 places: $pi%.2f")
    println(f"padded: ${42}%8d")

    // printf works too, inherited straight from the Java platform
    printf("hex: %x, octal: %o%n", 255, 8)

    // Console.err keeps diagnostics out of piped stdout
    Console.err.println("this goes to standard error")
  }
}

The f interpolator is worth adopting as your default for numeric formatting: it reads like an interpolated string but carries the full power of java.util.Formatter specifiers (%.2f, %8d, %x, and so on), with the compiler verifying that each expression’s type matches its specifier.

Reading from Standard Input

Console input goes through scala.io.StdIn, which wraps standard input in simple line-oriented methods. readLine() returns the next line without its trailing newline—one small convenience over Java’s raw streams. Converting text to a number is just a method call on the string: "42".toInt. Note that toInt throws on malformed input; in a program that must survive bad input you would wrap the conversion in Try, exactly as the file example below does for a missing file.

Create a file named ReadInput.scala:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import scala.io.StdIn

object ReadInput {
  def main(args: Array[String]): Unit = {
    println("What is your name?")
    val name = StdIn.readLine()

    println(s"Hello, $name! Enter two numbers, one per line.")
    val a = StdIn.readLine().trim.toInt
    val b = StdIn.readLine().trim.toInt
    println(s"Their sum is ${a + b}")
  }
}

Everything here is an expression: each readLine() result flows straight into an immutable val, gets trimmed and parsed in one chain, and the sum is computed inline inside the interpolated string. There’s no mutable accumulator in sight—a small example of how Scala’s functional style shapes even mundane console code.

File I/O with Source, Using, and Try

For files, idiomatic Scala reads through scala.io.Source and writes through the Java classes it inherits from the JVM (PrintWriter, FileWriter). The interesting part is resource safety: Using.resource takes anything closeable, runs your function on it, and guarantees close() is called even if the body throws—Scala’s answer to Java’s try-with-resources. And when an operation might fail, Try converts the exception into a value you can pattern match on.

Create a file named FileIo.scala:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.io.{FileWriter, PrintWriter}
import scala.io.Source
import scala.util.{Failure, Success, Try, Using}

object FileIo {
  def main(args: Array[String]): Unit = {
    // Write a file; Using closes the writer even if the body throws
    Using.resource(new PrintWriter("journal.txt")) { out =>
      out.println("day 1: learned println")
      out.println("day 2: learned val and var")
    }

    // Append by wrapping a FileWriter opened in append mode
    Using.resource(new PrintWriter(new FileWriter("journal.txt", true))) { out =>
      out.println("day 3: learned Using")
    }

    // Read the whole file into one String
    val contents = Using.resource(Source.fromFile("journal.txt"))(_.mkString)
    println(s"--- journal.txt (${contents.length} characters) ---")

    // Read line by line - Source is an iterator, so collection methods just work
    Using.resource(Source.fromFile("journal.txt")) { source =>
      for ((line, i) <- source.getLines().zipWithIndex)
        println(s"${i + 1}: $line")
    }

    // A missing file becomes a Failure value instead of an unhandled exception
    Try(Using.resource(Source.fromFile("missing.txt"))(_.mkString)) match {
      case Success(_) => println("missing.txt exists after all")
      case Failure(e) => println(s"could not read missing.txt: ${e.getMessage}")
    }

    new java.io.File("journal.txt").delete()
  }
}

Because Source is an Iterator[Char], reading a file feels like working with any Scala collection: mkString slurps it whole, getLines() yields an iterator of lines, and zipWithIndex numbers them—no manual loop counters. The final match on Try is the functional alternative to try/catch: the failure case is handled in the same pattern-matching style used everywhere else in Scala, and the program continues instead of crashing.

Running with Docker

You can run all three examples with the Scala CLI image—no local Scala installation required.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Pull the Scala CLI Docker image
docker pull virtuslab/scala-cli:latest

# Run the formatted output example
docker run --rm -v $(pwd):/app -w /app virtuslab/scala-cli:latest run FormattedOutput.scala

# Run the stdin example, piping in three lines of input
printf 'Ada\n2\n40\n' | docker run --rm -i -v $(pwd):/app -w /app virtuslab/scala-cli:latest run ReadInput.scala

# Run the file I/O example
docker run --rm -v $(pwd):/app -w /app virtuslab/scala-cli:latest run FileIo.scala

The stdin example needs Docker’s -i flag so the piped input reaches the program; without it, readLine() sees an immediate end-of-file.

Expected Output

Running FormattedOutput.scala (the stderr line appears interleaved in a terminal):

Loading...done
Scala first appeared in 2004
pi to 2 places: 3.14
padded:       42
hex: ff, octal: 10
this goes to standard error

Running ReadInput.scala with the piped input Ada, 2, 40:

What is your name?
Hello, Ada! Enter two numbers, one per line.
Their sum is 42

Running FileIo.scala:

--- journal.txt (71 characters) ---
1: day 1: learned println
2: day 2: learned val and var
3: day 3: learned Using
could not read missing.txt: missing.txt (No such file or directory)

Key Concepts

  • Two toolboxes, one language — Scala programs use java.io classes like PrintWriter directly while layering Scala-native APIs (Source, StdIn, Using) on top; you pick whichever fits.
  • The f interpolator is a type-checked printff"$pi%.2f" gives you the full formatter mini-language, and a mismatched specifier is a compile-time error rather than a runtime exception.
  • Console.err separates diagnostics from output — write errors to stderr so your program’s real stdout can be piped cleanly to files or other programs.
  • Source turns files into iteratorsmkString reads a whole file, getLines() streams it line by line, and collection methods like zipWithIndex work on files exactly as they do on lists.
  • Using.resource guarantees cleanup — it closes the file, writer, or any AutoCloseable even when the body throws, replacing hand-written finally blocks.
  • Try turns exceptions into values — a fallible read becomes a Success or Failure you pattern match on, letting recovery logic use the same style as the rest of your Scala code.
  • readLine() strips the newline — unlike Java’s raw streams, scala.io.StdIn hands you the line content directly; you typically still trim before parsing numbers.
  • Appending needs an explicit flagnew PrintWriter("file") truncates; wrap a new FileWriter("file", true) to append instead.

Running Today

All examples can be run using Docker:

docker pull virtuslab/scala-cli:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining