Intermediate

I/O Operations in Kotlin

Read from the console, write and read files, format output, and handle I/O errors in Kotlin with Docker-ready examples

Input and output are how a program talks to the outside world—printing results, reading what a user types, and moving data in and out of files. You already met println() in Hello World; this tutorial goes deeper into console I/O, formatted output, and file handling.

Because Kotlin runs on the JVM, it has full access to Java’s mature java.io and java.nio libraries. But Kotlin layers a set of concise, idiomatic extension functions on top of them. Where Java might need a BufferedReader, a try/finally, and several lines of ceremony, Kotlin often gets the job done in one expression like File("data.txt").readText().

Kotlin’s multi-paradigm nature shows up clearly in I/O. You can write imperative loops that read line by line, or functional pipelines that stream lines through map and filter. The use function brings automatic resource management (like Java’s try-with-resources) in a clean, functional style. In this tutorial you’ll write a program that prints formatted output and reads and writes files, then handle input and I/O errors safely.

Console Output and Formatting

print() writes without a trailing newline; println() adds one. For anything beyond simple concatenation, Kotlin borrows Java’s printf-style format specifiers through the String.format extension, called simply as "...".format(...).

Create a file named IoOperations.kt:

 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
import java.io.File

fun main() {
    // print() has no newline; println() adds one
    print("No newline here. ")
    println("This ends the line.")

    // Values concatenate directly into strings
    println("Numbers: " + 42 + ", " + 3.14)

    // Formatted output with printf-style specifiers
    val pi = 3.14159265
    println("Pi to 2 decimals: %.2f".format(pi))
    println("Padded: %5d|".format(42))       // right-aligned in 5 columns
    println("Name: %-10s|".format("Kotlin"))  // left-aligned in 10 columns

    // Write text to a file (creates or overwrites)
    val file = File("greetings.txt")
    file.writeText("Hello from Kotlin\n")
    file.appendText("Second line\n")

    // Read the whole file back as a single string
    val content = file.readText()
    print(content)

    // Read the file as a list of lines and number them
    println("--- lines ---")
    file.readLines().forEachIndexed { index, line ->
        println("${index + 1}: $line")
    }
}

Here writeText creates (or overwrites) the file, appendText adds to it, readText pulls the entire contents into a String, and readLines returns a List<String>—one entry per line. These extension functions handle opening and closing the file for you.

Reading Console Input

readLine() reads a single line from standard input and returns a String?—nullable, because input can reach end-of-file. Kotlin’s null-safety features make you handle that case explicitly.

Create a file named EchoInput.kt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fun main() {
    print("What is your name? ")
    val name = readLine()

    if (name.isNullOrBlank()) {
        println("No name provided.")
    } else {
        println("Hello, $name!")
    }
}

isNullOrBlank() is a standard-library extension that safely checks for null, empty, or whitespace-only input in one call. To convert typed input to a number, chain toIntOrNull(), which returns null instead of throwing on bad input.

Buffered I/O with Automatic Cleanup

For larger files you want buffering and guaranteed resource cleanup. The use function runs a block and closes the resource afterward—even if an exception is thrown—mirroring Java’s try-with-resources. useLines streams a file lazily, so you never hold the whole thing in memory.

Create a file named BufferedIo.kt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import java.io.File

fun main() {
    // Buffered writing; 'use' closes the writer automatically
    File("numbers.txt").bufferedWriter().use { writer ->
        for (i in 1..5) {
            writer.write("Line $i")
            writer.newLine()
        }
    }

    // useLines streams the file lazily and closes it when done
    var total = 0
    File("numbers.txt").useLines { lines ->
        lines.forEach { _ -> total += 1 }
    }

    println("Read $total lines")
}

Handling I/O Errors

File operations can fail: a file may be missing, unreadable, or locked. Reading a nonexistent file throws a FileNotFoundException. Wrap risky calls in a try/catch and respond gracefully instead of crashing.

Create a file named SafeRead.kt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import java.io.File
import java.io.FileNotFoundException

fun main() {
    val file = File("missing.txt")
    try {
        val text = file.readText()
        println(text)
    } catch (e: FileNotFoundException) {
        println("File not found: ${file.name}")
    }
}

Because missing.txt does not exist, readText() throws, the catch block runs, and the program reports the problem cleanly rather than terminating with a stack trace.

Running with Docker

The zenika/kotlin:1.4 image compiles a .kt file to a runnable JAR and executes it on the JVM. The -v $(pwd):/app mount lets the program create files (greetings.txt, numbers.txt) in your current directory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the official image
docker pull zenika/kotlin:1.4

# Compile and run the main I/O example
docker run --rm -v $(pwd):/app -w /app zenika/kotlin:1.4 \
  sh -c "kotlinc IoOperations.kt -include-runtime -d io.jar && java -jar io.jar"

# Run the buffered I/O example
docker run --rm -v $(pwd):/app -w /app zenika/kotlin:1.4 \
  sh -c "kotlinc BufferedIo.kt -include-runtime -d buffered.jar && java -jar buffered.jar"

# Run the error-handling example
docker run --rm -v $(pwd):/app -w /app zenika/kotlin:1.4 \
  sh -c "kotlinc SafeRead.kt -include-runtime -d safe.jar && java -jar safe.jar"

For the console-input example, add -i to keep stdin open and pipe a value in:

1
2
3
# Pipe input into readLine() with an interactive (-i) container
echo "Ada" | docker run --rm -i -v $(pwd):/app -w /app zenika/kotlin:1.4 \
  sh -c "kotlinc EchoInput.kt -include-runtime -d echo.jar && java -jar echo.jar"

Expected Output

Running IoOperations.kt produces:

No newline here. This ends the line.
Numbers: 42, 3.14
Pi to 2 decimals: 3.14
Padded:    42|
Name: Kotlin    |
Hello from Kotlin
Second line
--- lines ---
1: Hello from Kotlin
2: Second line

Running BufferedIo.kt produces:

Read 5 lines

Running SafeRead.kt produces:

File not found: missing.txt

Piping Ada into EchoInput.kt produces:

What is your name? Hello, Ada!

Key Concepts

  • Extension functions simplify I/OwriteText, appendText, readText, and readLines on File handle opening and closing for you, replacing Java’s stream boilerplate.
  • readLine() returns a nullable String? — Kotlin forces you to handle end-of-file, and helpers like isNullOrBlank() and toIntOrNull() make that safe and concise.
  • use guarantees cleanup — it closes any Closeable (writers, readers) after the block finishes, even on exceptions, just like try-with-resources.
  • useLines streams lazily — read huge files without loading them entirely into memory, then let the resource close automatically.
  • Formatted output uses printf specifiers — call "%.2f".format(x) for decimals, %5d for width, and %-10s for left alignment.
  • I/O errors are exceptions — wrap file reads in try/catch for FileNotFoundException and related types to fail gracefully.
  • Full JVM interop — anything in Java’s java.io and java.nio is available, so you never outgrow Kotlin’s concise helpers.

Running Today

All examples can be run using Docker:

docker pull zenika/kotlin:1.4
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining