CodeGym /Courses /Kotlin SELF /I/O errors: IOException and FileNotFoundException

I/O errors: IOException and FileNotFoundException

Kotlin SELF
Level 45, Lesson 0
Available

1. Why file operations break

When you write code, it is really tempting to believe the computer is an obedient calculator: you say “read the file” — it reads it. In practice, a computer is closer to a cat: sometimes it pretends you do not exist. The file could have been deleted, moved, access could have been revoked, a folder could have been replaced by a file (yes, that happens), the disk could run out of space, an antivirus could block the operation, and on Windows a file can be “in use” by another process.

Key idea: I/O (input/output) is a high-uncertainty zone. In arithmetic, 2 + 2 is almost always 4. With files, “read text” can mean “try to negotiate with the OS, the file system, permissions, and the current disk state”. And if that “negotiation” fails — you get an exception.

exists() is not a guarantee

The most common logical trap for beginners sounds like this: “I checked exists(), so readText() will definitely work”. Unfortunately, no. Between the check and the actual read operation, the world can change. The file could be deleted, permissions could change, the directory could be renamed, the disk could disappear (yes, sometimes literally).

This is the classic effect of “checked — and only then learned the truth”. So the rule is simple and a bit philosophical: checks are useful for diagnostics and early messages, but you still need try/catch around real I/O.

Bad (naive) code:

import java.io.File

fun main() {
    val file = File("data/input.txt")

    if (file.exists()) {
        val text = file.readText() // can still crash
        println(text.length)
    } else {
        println("File does not exist")
    }
}

A more mature minimum (check + protection):

import java.io.File
import java.io.IOException

fun main() {
    val file = File("data/input.txt")
    println("Before reading: exists=${file.exists()}")

    try {
        val text = file.readText()
        println("Read: ${text.length}") // for example: Read: 120
    } catch (e: IOException) {
        println("Read failed: ${file.path}")
    }
}

The point is not to “smear everything with checks”, but to accept this: a check is a hint, while try/catch is the last line of defense.

2. IOException and FileNotFoundException on the JVM

Before catching exceptions, it helps to understand what we are catching at all. In Kotlin (on the JVM), exceptions are objects, and all of them are descendants of Throwable. Next come Error and Exception, and inside Exception live various “runtime” problems that we can usually handle.

Where IOException sits in the hierarchy

A very practical way to read this fact is: IOException is an “umbrella” for everything related to reading/writing/opening streams. You are not required to remember every specific subclass, but it is important to understand the style: if you do real I/O, it can throw IOException.

Mini-example “catching the umbrella”:

import java.io.File
import java.io.IOException

fun main() {
    val file = File("data/input.txt")

    try {
        val text = file.readText()
        println("OK: ${text.length}") // for example: OK: 42
    } catch (e: IOException) {
        println("I/O error while reading: ${file.path}")
        println("Reason: ${e.message}")
    }
}

Even if you know nothing about file systems, you can already make your program noticeably more resilient — not “crash”, but report what went wrong.

FileNotFoundException: “file not found” and other surprises

The name FileNotFoundException sounds like the cause is always one: “file not found”. And sometimes that is true. But in reality this type often means a broader category: “failed to open the path as a file”.

That is, you might be in one of these scenarios: the file really does not exist, the path points to a directory, the process does not have read permissions, or the file exists but access is denied/impossible at the OS level. For a beginner programmer, this is unpleasant: you checked with your eyes that the file is “there”, and the program still says “not found”. What it means is: “I could not open this path as a readable file”.

At this point it is useful to start thinking like a diagnostician: we do not argue with the exception (“but it exists!”), we clarify reality: what object is at that path and what basic properties it has right now.

Here is a minimal check that often resolves half the questions immediately:

import java.io.File

fun main() {
    val file = File("data/input.txt")

    println("exists=${file.exists()}")       // exists=true/false
    println("isFile=${file.isFile}")         // isFile=true/false
    println("isDir=${file.isDirectory}")     // isDir=true/false
}

If you see exists=true, but isFile=false and isDir=true, then you tried to read a directory as a file. This is not a “Kotlin bug”, it is a “expectations bug”.

A careful try/catch: specific → general

Many beginners want to catch Exception right away, because “that will definitely catch everything”. The problem is that afterwards you effectively turn off your own brain: you do not build the habit of distinguishing causes. Kotlin (and the JVM) encourage you to catch more specific exceptions first, and a more general type later. That is why the order of catch matters: from more specific to more general.

A good basic template for reading:

import java.io.File
import java.io.FileNotFoundException
import java.io.IOException

fun main() {
    val file = File("data/input.txt")

    try {
        val text = file.readText()
        println("OK, length=${text.length}")
    } catch (e: FileNotFoundException) {
        println("Not found / cannot be opened as a file: ${file.path}")
        println("Diagnostics: ${describePath(file)}")
    } catch (e: IOException) {
        println("I/O error while reading: ${file.path}")
        println("Reason: ${e::class.simpleName}: ${e.message}")
    }
}

What matters here is that we do not pretend these are “the same errors”. If the file cannot be opened as a file — we say that separately. If it is “some more general I/O” — we can still report a clear minimum.

3. Diagnosing I/O errors

When you catch IOException, you see the tip of the iceberg: “failed”. The user (and future you) needs to understand why. The good news: there are not that many typical causes, and it is convenient to keep them in mind as a map.

Mini-map of causes

Below is a small table (without trying to cover the whole universe) that helps you quickly “make a diagnosis”:

What you tried to do Common real cause What it looks like
Read a file No file / wrong path
FileNotFoundException
Read a file The path is a directory FileNotFoundException or IOException
Read a file No read permission often FileNotFoundException or AccessDenied… in the message
Write a file No parent directory sometimes FileNotFoundException (because opening for writing failed)
Write a file Disk is “full” IOException, message may hint at space/quota
Read/write File is in use/locked more often IOException, depends on OS

Right now our goal is not “to learn how to fix everything in the world”, but to learn to correctly distinguish at least two big situations:

1) “Not found / not opened as a file” (often FileNotFoundException)
2) “Opened, but then reading/writing broke” (often the general IOException)

And we build handling and messages around that.

What to print so you do not guess

There is a temptation to do this: catch an exception — print “Something went wrong” — go drink tea. But that is a path to eternal tea and eternal pain. Diagnostics in file operations is almost always built around three things: action, path, brief properties of the object at the path.

Imagine you were sent a screenshot of an error (without context). What would you want to see? Usually this: “I tried READ or WRITE”, “here is the path”, “here is what was there”, “here is the exception type and message”. That is enough to understand the cause in most cases.

Let us make a small function that describes a path. This is not “future architecture”, just a convenient printable label:

import java.io.File

fun describePath(file: File): String {
    return "path=${file.path}, exists=${file.exists()}, isFile=${file.isFile}, isDir=${file.isDirectory}"
}

fun main() {
    val file = File("data/input.txt")
    println(describePath(file))
    // for example: path=data/input.txt, exists=false, isFile=false, isDir=false
}

Now, when an error happens, you can print one line and dramatically increase your chances of quickly understanding the cause.

Example: reading expenses without “crashing” the app

So this does not stay theory, let us mentally continue our learning console app (say it is a simple expense tracker). We want to learn to read the file data/expenses.txt so the data survives restarting the program.

And then suddenly it turns out: if the file does not exist, it is not a “catastrophe”, it is a normal situation. For example, the user runs the program for the first time. So we need a clear contract: either we read the lines, or we return an empty list and print diagnostics.

Let us make a function that does not crash, but returns a list of strings. Right now we are practicing specifically diagnostics for read errors, without atomic writing and without complex reliability.

import java.io.File
import java.io.FileNotFoundException
import java.io.IOException

fun loadExpenseLines(file: File): List<String> {
    return try {
        file.readLines()
    } catch (e: FileNotFoundException) {
        println("No expenses file: ${file.path}")
        emptyList()
    } catch (e: IOException) {
        println("Failed to read expenses: ${file.path}")
        println("Reason: ${e.message}")
        emptyList()
    }
}

And use it in main:

import java.io.File

fun main() {
    val file = File("data/expenses.txt")
    val lines = loadExpenseLines(file)

    println("Expense lines: ${lines.size}") // for example: Expense lines: 0
}

Why is this a good style for a beginner? Because the app keeps running. It can show a menu, let the user add an expense, and only then save the file (about saving and reliability — later). And most importantly: if something goes wrong, you already see which file and what exactly failed.

4. Typical mistakes when working with IOException and diagnostics

Mistake #1: catch Exception and write “something went wrong”.
Such code is formally “resilient”, but practically useless: you hid the cause from yourself. The minimum quality standard is to mention the action (read/write), the path (file.path), and e.message. Otherwise you turn debugging into guesswork.

Mistake #2: treat FileNotFoundException as a literal “file is missing”.
Beginners often argue with the exception: “but it is there!”. In reality this often means “failed to open as a file”: the path can point to a directory, access can be denied, or the file exists but cannot be opened under current conditions. That is why for this exception it is especially useful to print diagnostics exists/isFile/isDirectory.

Mistake #3: think exists() guarantees a successful read/write.
An existence check is a snapshot of the world at one specific millisecond. After that, the world can change. So checks are good as an early warning and as part of a message, but the real operation must still be inside try/catch, because that is what collides with reality.

Mistake #4: lose the context of “what we were doing” and “with what”.
If an app has multiple files (for example, data/expenses.txt, data/categories.txt, data/report.txt), then the message “Failed to read file” without the path turns into a riddle. The rule is simple: every I/O error message must include the path and the operation. Then even a user (not a programmer) can retell the problem to you properly.

Mistake #5: mix an I/O error and a data error into one pot.
If the file was read, but it contains garbage (for example, a line cannot be parsed), that is a different category of problems: “data format”. Today we intentionally keep the focus on I/O: “read/did not read”, “wrote/did not write”. If you mix this into one catch, later it will be hard to understand what to fix: the path or the parser.

Mistake #6: pretend the exception message is not important.
Sometimes a developer prints only the exception type and forgets about e.message. But that is exactly where the OS often leaves the key hint: access denied, no such file, disk full, and so on. Even if the text is in English, it is still better than total silence.

1
Task
Kotlin SELF, level 45, lesson 0
Locked
File length without crashing the program
File length without crashing the program
1
Task
Kotlin SELF, level 45, lesson 0
Locked
Separate messages for FileNotFoundException and IOException
Separate messages for FileNotFoundException and IOException
1
Task
Kotlin SELF, level 45, lesson 0
Locked
The describePath(file) function and diagnostic reading from the entered path
The describePath(file) function and diagnostic reading from the entered path
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION