Skip to main content
Version: 2.x

Console

The Console service contains simple I/O operations for reading/writing strings from/to the standard input, output, and error console. It follows the shared service pattern common to all of ZIO's built-in services: a Console trait, a Live implementation, and a synchronous UnsafeAPI for interop.

FunctionInput TypeOutput Type
printline: => AnyZIO[Any, IOException, Unit]
printErrorline: => AnyZIO[Any, IOException, Unit]
printLineline: => AnyZIO[Any, IOException, Unit]
printLineErrorline: => AnyZIO[Any, IOException, Unit]
readLineZIO[Any, IOException, String]
readLineprompt: StringZIO[Any, IOException, String]

Unlike Clock and Random, whose methods mostly return UIO and cannot fail, every Console operation can fail with IOException, since reading from or writing to the console is a real I/O operation that can go wrong.

readLine(prompt) is a convenience overload defined as a default method on the Console trait itself, combining print and readLine:

trait Console {
def readLine(prompt: String): IO[IOException, String] =
print(prompt) *> readLine
}

All functions of the Console service are effectful, this means they are just descriptions of reading/writing from/to the console.

As ZIO data type supports monadic operations, we can compose these functions with for-comprehension which helps us to write our program pretty much like an imperative program:

import java.io.IOException

import zio._
import zio.Console._

object MyHelloApp extends ZIOAppDefault {
val program: ZIO[Any, IOException, Unit] = for {
_ <- printLine("Hello, what is you name?")
name <- readLine
_ <- printLine(s"Hello $name, welcome to ZIO!")
} yield ()

def run = program
}

Note again, every line of our program are descriptions, not statements. As we can see the type of our program is ZIO[Any, IOException, Unit], it means to run program we do not need any environment, it may fail due to failure of readLine and it will produce Unit value.

Blocking I/O​

ConsoleLive implements print and printLine with ZIO.attemptBlockingIO, and readLine with ZIO.attemptBlockingInterrupt — both run the underlying, genuinely blocking system call on ZIO's blocking thread pool rather than on the fiber's regular executor. This keeps a slow console write or a read waiting on user input from starving other fibers, but the operation itself is still blocking work, not a lightweight async one — avoid calling these in a tight loop where throughput matters.

readLine fails with EOFException (a subtype of IOException) when standard input is exhausted — for example, when a program's stdin is piped from a file or another process that has finished producing input. Handle it explicitly when a missing line is expected rather than a bug:

import zio._
import java.io.EOFException

val nextLineOrDefault: ZIO[Any, java.io.IOException, String] =
Console.readLine.catchSome { case _: EOFException =>
ZIO.succeed("<no input>")
}

Synchronous Access (unsafe)​

Console also exposes a synchronous UnsafeAPI, following the pattern described in Anatomy of a Built-in Service. ConsoleLive's unsafe implementation calls scala.io.StdIn.readLine() directly for input (throwing EOFException on a null result) and writes through scala.Console, without running an effect:

import zio._

Unsafe.unsafe { implicit unsafe =>
Console.ConsoleLive.unsafe.printLine("hello")
}

Prefer the ordinary Console.printLine/Console.readLine accessors everywhere else.

Testing Console I/O​

TestConsole lets tests feed predetermined input and capture output instead of touching the real console. See Testing Console for its feedLines, output, and debug/silent API.

See Also​

  • Built-in Services — Guide to ZIO's built-in services: Console, Clock, Random, and System with automatic environment management.