1. Introduction
When we say “read a file”, what we really mean is: “we need to explain to the operating system exactly where this file is located”. That’s what a path is for — a string (or object) that describes the address of a file/folder in the file system. In practice, a path often looks like "data/input.txt" or "C:\\Projects\\App\\data\\input.txt" — and that’s exactly where the adventures begin.
The problem is that the same path string can refer to different places on disk depending on where the program is launched from. So the topic of paths isn’t pedantry — it’s a way to save yourself a couple of hours of life and one nervous tic. Because the error “file not found” usually doesn’t mean the file is missing, but that the program is looking in the wrong place.
Absolute path
An absolute path is a path that fully describes the location of a file, starting from the “root” of the file system. It does not depend on where you are or where you launched the program from. If an absolute path is correct, then the file either exists at that location or it doesn’t — no “magic” and no “well, it worked on my machine”.
On Windows, an absolute path usually starts with a drive letter (for example, C:\...), while on macOS/Linux it starts with /.... But the idea matters more: an absolute path is an address with city, street, building, and apartment, not “roughly somewhere near home”.
In Kotlin/JVM we often work with absolute paths via java.io.File, and one of the most useful diagnostic tricks is to look at absolutePath.
import java.io.File
fun main() {
val f = File("data/input.txt")
println("relative = ${f.path}") // relative = data/input.txt
println("absolute = ${f.absolutePath}") // absolute = .../data/input.txt
}
In this example, the file may not even exist — and that’s fine. File("...") here does not “open the file”; it simply stores the path and can display it in different forms.
Relative path
A relative path is a path that is not a full address and is resolved “from somewhere”. And that “somewhere” is the key to the whole puzzle.
For example, the path "data/input.txt" means: “find the data folder in the process’s current working directory, and inside it the file input.txt”.
So a relative path is like the phrase: “the store around the corner”. It works as long as we know which corner. But if you “teleported” to another neighborhood (launched the program differently), then “around the corner” suddenly became a completely different corner.
Relative paths are very convenient because they make a project portable: you can keep data next to the project, commit the folder structure, and avoid hardcoding C:\Users\... into the code. But you have to use them correctly: always remember what the current working directory is.
2. Working directory and file lookup
Working directory
The working directory is the folder relative to which relative paths are interpreted. This is one of the most common reasons why everything works in the IDE but when you run from the terminal you get “file not found” — or vice versa.
Checking “where we are right now” is as simple as it gets:
import java.io.File
fun main() {
val wd = File(".").absolutePath
println("Working dir = $wd") // Working dir = ... (absolute path of the current folder)
}
File(".") means “current folder”. And absolutePath is a way to see what exactly the JVM considers the current working directory in this run.
Why is this so important? Because you can honestly write File("data/input.txt"), be sure the data folder exists, and still get “not found” — if the program was launched from a different directory and is looking for data somewhere else than you expected.
Why it can differ between the IDE and the terminal
There’s a classic storyline.
You build a project with this structure:
MyApp/
src/
data/
input.txt
You run it from the IDE — and the IDE may set the working directory to MyApp/. The relative path "data/input.txt" resolves to MyApp/data/input.txt — everything is fine.
Then you run it differently: for example, from a terminal not from MyApp/ but from the parent folder or even from an entirely different directory. Then "data/input.txt" resolves to some_other_folder/data/input.txt — and the file, of course, isn’t there.
The same logic appears not only when launching a program, but also with any tools that read files via relative paths: relative paths are almost always resolved from the process’s current working directory.
The moral is simple: a relative path is not “a path inside the project”, but “a path from the working directory you started from”.
Diagnosing “file not found”
When you see FileNotFoundException or you simply realize the program “can’t see the file”, a beginner’s first reaction is: “why can’t Kotlin find the file, it’s right there!”. A slightly more experienced reaction is: “where is the program looking for the file?”.
And here’s the golden mini-template for diagnostics:
import java.io.File
fun main() {
val relative = "data/input.txt"
val f = File(relative)
println("Working dir = ${File(".").absolutePath}")
println("Trying file = ${f.absolutePath}")
}
The point is that you get two important clues:
- Working dir — the reference point for relative paths
- Trying file — the absolute path the program is actually using
After that, the “magic” disappears: you literally see the address where the program is searching for the file. And then it turns out you expected .../MyApp/data/input.txt, but got .../MyApp/src/data/input.txt or .../SomeOtherFolder/data/input.txt.
Mini diagram: how a relative path becomes absolute
Words are words, but it’s useful to see it once as a process. Roughly:
flowchart TD
A["Relative path: data/input.txt"] --> B["Process working directory: WD"]
B --> C["Combine: WD + data/input.txt"]
C --> D["Absolute path: /.../WD/data/input.txt"]
This diagram does not mean “simple string addition” at the OS level, but for our purposes that’s the idea: a relative path becomes absolute through the working directory.
3. File as a path object and building paths
File(path) is a path object
The word File is misleading: it feels like File("data/input.txt") “opens the file”. In reality, in Java/Kotlin File is first and foremost a path object. It stores a path string and can:
- return path (as you provided it),
- return absolutePath (as the OS will see this path),
- later — check existence, read, write, and so on.
So File is like an “Address” object, not like “the contents of a package”. You can create File("data/input.txt") even if the file doesn’t exist: it’s just “the address you want to use”.
How not to glue paths with strings
When a beginner first starts working with files, there’s a strong urge to do this:
val path = "data/" + "input.txt"
And that’s still relatively harmless. Then it becomes:
val path = "data" + "/" + "input.txt"
And then suddenly: Windows, backslashes, double slashes, spaces — and everything turns into an archaeological dig.
Even within this lecture (even before Path.of(...), which comes later) you can do it much more neatly: build the path via File(parent, child). This isn’t a “magic” function; it’s just a safer way to combine parts.
import java.io.File
fun main() {
val dir = File("data")
val file = File(dir, "input.txt")
println(file.path) // data/input.txt
println(file.absolutePath) // .../data/input.txt
}
This reads as “the file input.txt inside the directory data”, not “a string assembled by shamanic rituals”.
4. Embedding paths into an app and making output useful
“Where the project data lives”
To keep the examples from being abstract, let’s imagine our practical CLI app: say it’s a small expense tracker. It used to keep everything in memory, and now we want to store data in the data/ folder.
Today we still won’t read or write (that’s for the next lectures), but we can take an important step: centralize the place where the program “expects” the data file, and add diagnostics.
import java.io.File
private fun dataFile(): File {
return File("data/expenses.txt")
}
fun main() {
val f = dataFile()
println("Working dir = ${File(".").absolutePath}")
println("Data file (relative) = ${f.path}") // Data file (relative) = data/expenses.txt
println("Data file (absolute) = ${f.absolutePath}") // Data file (absolute) = .../data/expenses.txt
}
Here we did two good things at once.
First, the path to the file is now in one place, and later (when we start actually reading/writing) we won’t end up with “in one place data/expenses.txt, in another ./data/expenses.txt, and in a third it’s data\expenses.txt”.
Second, printing Working dir and the absolute path to the file is built-in diagnostics. When you hit “file not found”, you’ll already see where the program is looking.
Print the path so it can be checked manually
When you print paths, it’s useful to print them in a way that a person can literally copy and paste, open in a file explorer/terminal, and verify. In other words, it’s better to print the absolute path and a short explanation next to it.
import java.io.File
fun main() {
val reportFile = File("data/reports/today.txt")
println("I will look for report at:")
println(reportFile.absolutePath)
// .../data/reports/today.txt
}
Yes, it looks too simple. But this is exactly the kind of case where simplicity is not a weakness — it’s a sign you don’t want to debug an “invisible” path.
5. Common mistakes when working with paths
Mistake No. 1: assuming a relative path is resolved from the folder with the code.
A very logical assumption, because in your head project = the folder with src. But the JVM doesn’t think that way. A relative path is resolved from the working directory at launch, which depends on the IDE/terminal/Run Configuration settings. The best remedy is to print File(".").absolutePath when you have problems.
Mistake No. 2: confusing “path inside the project” and “path at runtime”.
The data/ folder may sit next to src/, but if the working directory points to src/, then data/input.txt will be expected as src/data/input.txt. From the outside it looks like the path is “correct”, but it’s only correct in your head. The absolute path via absolutePath brings you back to reality fast.
Mistake No. 3: gluing paths with strings and running into weird slashes.
String concatenation quickly leads to double separators, missing /, and “fixed on Windows — broke on Linux”. Even if you’re not using Path.of(...) yet, building a path via File(parent, child) already makes the code noticeably more reliable and readable.
Mistake No. 4: trying to diagnose file reading without diagnosing the path.
Beginners often jump straight into try/catch, catch an exception, and print “read error”. But without printing the working directory and the absolute path, that message is almost useless: you know it’s “not found”, but you don’t know “where you looked”. The right order of thinking is: first figure out “where are we pointing”, then deal with reading.
Mistake No. 5: keeping the file path smeared across the whole project.
When the string "data/expenses.txt" appears in five places, one day you’ll change it in four and get a bug that looks like mysticism: “sometimes it reads the old one, sometimes it writes to the new one”. Life is much calmer when the path is created in one place (a function or a constant) and then passed around the code as a File.
GO TO FULL VERSION