Compiling and Running Kotlin
Before any Kotlin program can run, its human-readable source code has to become something the Java Virtual Machine (JVM) understands: bytecode. The Kotlin compiler, kotlinc, is the tool that performs that translation, and the kotlin command (or plain java) is what actually executes the result. Understanding this compile-then-run pipeline — not just clicking a “Run” button in an IDE — is what lets you build, package, and ship real Kotlin programs, debug build errors, and understand what your IDE is quietly doing every time you hit run.
Overview: How Kotlin Source Becomes a Running Program
Kotlin is a statically typed language that, in its most common configuration, compiles to JVM bytecode — the same instruction format produced by javac when compiling Java. This is why Kotlin code can call Java libraries directly, and why a compiled Kotlin program runs on any machine with a Java Virtual Machine installed. The compiler that performs this translation is kotlinc, which ships with the official Kotlin command-line tools (and is what IntelliJ IDEA and Gradle invoke behind the scenes when you click “Run”).
A Kotlin source file’s entry point is a top-level function named main. Unlike Java, Kotlin does not require you to wrap it in a class — you can write fun main() { ... } directly in a file and nothing else is mandatory. Behind the scenes, the compiler still needs a JVM class to hold that function as a static method, so it generates one automatically from the file name: a file named HelloWorld.kt produces a class called HelloWorldKt containing a static main method. You rarely need to know this class name — the kotlin launcher and your IDE find it for you — but it becomes directly relevant the moment you build a jar by hand or see it appear in a stack trace.
main may be declared two ways: fun main() with no parameters, or fun main(args: Array<String>), which receives the command-line arguments passed after the program name. Both are valid entry points; use the version with args only when your program actually reads command-line input.
There are three common ways to turn source into a running program, and it helps to know all three even if you’ll use an IDE day to day:
- Compile to class files, then run with the
kotlinlauncher. Fast, and the launcher automatically puts the Kotlin runtime on the classpath for you. - Compile to a self-contained jar, then run with plain
java. Slower to build but produces something you can hand to anyone with a JVM — no separate Kotlin installation required. - Use the REPL or an online playground for one-off snippets, skipping files entirely.
Real projects almost never invoke kotlinc by hand — a build tool like Gradle (with the Kotlin plugin) tracks source files, dependencies, and output artifacts, and an IDE wires a “Run” button to that build. But every one of those tools is, underneath, running the same kotlinc compilation and JVM execution described here, and knowing the raw commands is what lets you understand build errors, write CI scripts, or debug why a jar won’t start.
Syntax: The Compile-and-Run Commands
The table below covers the core commands you’ll use from a terminal. All of them assume kotlinc and kotlin are installed and on your PATH (both come from the official Kotlin command-line tools).
| Command | What it does |
|---|---|
kotlinc HelloWorld.kt -d out |
Compiles the file to JVM .class files placed in the out directory. |
kotlin -classpath out HelloWorldKt |
Runs the compiled class. The Kotlin launcher automatically adds the Kotlin runtime to the classpath. |
kotlinc HelloWorld.kt -include-runtime -d app.jar |
Compiles straight to a single jar with the Kotlin standard library bundled inside it. |
java -jar app.jar |
Runs the jar with the plain JVM — works on any machine with Java installed, no Kotlin tools needed. |
kotlinc |
With no file argument, starts an interactive REPL where you can type and evaluate Kotlin one line at a time. |
kotlin script.kts |
Runs a .kts script file directly, with no separate compile step. |
Note the difference between a .kt file and a .kts file: a .kt file is a compiled source file that needs kotlinc before it can run, while a .kts file is a script that the kotlin command interprets and runs immediately, without you invoking the compiler yourself.
Examples
Example 1: Compiling and running “Hello, World”
Save the following as HelloWorld.kt:
fun main() {
println("Hello, Kotlin!")
println("Compiled with kotlinc, running on the JVM.")
}
Compile it, then run the result:
kotlinc HelloWorld.kt -d out
kotlin -classpath out HelloWorldKt
Output:
Hello, Kotlin!
Compiled with kotlinc, running on the JVM.
The first command produces out/HelloWorldKt.class (plus a couple of Kotlin metadata files). The second loads that class on the JVM and calls its main method — exactly the top-level fun main() you wrote. The compiler generated the class and the static method for you from the file’s contents.
Example 2: Reading command-line arguments
fun main(args: Array<String>) {
println("Program started with ${args.size} argument(s).")
for (arg in args) {
println("- $arg")
}
}
Compile it, then run it with two arguments:
kotlinc Args.kt -d out
kotlin -classpath out ArgsKt Kotlin Rocks
Output:
Program started with 2 argument(s).
- Kotlin
- Rocks
Everything after the class name on the kotlin command line becomes an element of the args array, in order. If you instead ran kotlin -classpath out ArgsKt with nothing after the class name, args would be an empty array — not null, Kotlin always supplies an empty Array<String>, never a null one — so args.size would print 0 and the loop body would simply never execute.
Example 3: A realistic multi-function file
A single file can hold many declarations; only one fun main() is needed as the entry point:
fun square(n: Int): Int = n * n
fun describe(n: Int): String {
val sq = square(n)
return "The square of $n is $sq"
}
fun main() {
val numbers = listOf(2, 3, 4)
for (n in numbers) {
println(describe(n))
}
}
Output:
The square of 2 is 4
The square of 3 is 9
The square of 4 is 16
Compiling this with kotlinc Numbers.kt -include-runtime -d numbers.jar and running it with java -jar numbers.jar produces the same output — the -include-runtime flag only changes how the program is packaged, not what it does. square and describe are ordinary top-level functions; they don’t need to live inside a class, and main can call them because everything in the file is compiled together.
How It Works, Step by Step
- Parsing and type-checking.
kotlincreads your.ktfile(s), builds a syntax tree, and resolves every type — this is where a nullability error, a typo in a function name, or a mismatched argument gets caught, before anything runs. - Bytecode generation. Once the code type-checks, the compiler emits JVM bytecode — the same low-level format
javacproduces for Java — as one or more.classfiles. - Entry-point naming. A top-level
fun main()inHelloWorld.ktbecomes apublic static void mainmethod on a generated class namedHelloWorldKt. - Packaging (optional). If you target a
.jarwith-d,kotlincwrites the class files into a jar with a manifest pointing at the detected main class. Adding-include-runtimealso copies the Kotlin standard library’s classes into that jar. - Loading. The
kotlinlauncher (or plainjava -jar) starts a JVM process, which loads the generated class from the classpath. - Execution. The JVM invokes the
mainmethod. Everyprintlncall inside it writes to standard output through the Kotlin standard library, which itself delegates to Java’sSystem.out.
Common Mistakes
Mistake 1: Writing statements outside any function
Coming from a scripting language, it’s tempting to put a statement at the top level of a .kt file expecting it to just run:
println("Hello from the top level")
fun main() {
println("Hello from main")
}
This fails to compile with an error like expecting a top level declaration. A .kt file’s top level may only contain declarations — functions, classes, properties, imports — not arbitrary statements like a bare function call. (This is different from a .kts script file, where top-level statements are allowed.) Move the statement inside a function:
fun main() {
println("Hello from the top level")
println("Hello from main")
}
Mistake 2: Trying to run a .kt file directly, like a script
The kotlin command’s single-step, no-compile-needed mode is for .kts scripts, not .kt source files. Pointing it straight at a .kt file, expecting behavior like python file.py or node file.js, is not the supported path:
kotlin HelloWorld.kt
The reliable path for a .kt file is always compile-then-run: kotlinc HelloWorld.kt -d out followed by kotlin -classpath out HelloWorldKt. If you genuinely want single-step execution without naming a class, rename the file to HelloWorld.kts and run kotlin HelloWorld.kts — that’s what script mode is for.
Mistake 3: Forgetting -include-runtime when building a standalone jar
A jar built without the Kotlin runtime bundled in it looks fine at compile time but fails the moment you try to run it standalone:
kotlinc HelloWorld.kt -d app.jar
java -jar app.jar
This typically throws NoClassDefFoundError: kotlin/jvm/internal/Intrinsics (or a similar missing-class error) at runtime, because the compiled bytecode references Kotlin standard library classes — used for things as basic as null-check helpers — that were never copied into the jar. java has no idea where to find them unless they’re already on its classpath. The fix is to bundle the runtime at compile time:
kotlinc HelloWorld.kt -include-runtime -d app.jar
java -jar app.jar
Best Practices
- Use
-include-runtimewhen a jar needs to run standalone with plainjava; skip it for local development where thekotlinlauncher already supplies the runtime. - Let a build tool (Gradle with the Kotlin plugin) or your IDE manage compilation for anything beyond a single throwaway file — real projects have too many source files, dependencies, and JVM targets to track
kotlincflags by hand. - Use the REPL (run
kotlincwith no arguments) or an online playground to test a small expression or API before committing it to a project file. - Keep exactly one
fun main()per file you intend to run directly; if several files in a project each definemain, be explicit about which generated class (FileNameKt) you’re compiling or running. - Set
-jvm-targetto match the Java version you actually deploy to (for example-jvm-target 17) — Kotlin always compiles to bytecode for a specific JVM version, and mismatches show up as “unsupported class file version” errors at runtime.
Practice Exercises
- Write a file named
Greeter.ktwith afun main()that printsHello, World!usingprintln. Compile it withkotlincand run the result with thekotlinlauncher. Then rebuild it as a standalone jar with-include-runtimeand run that jar withjava -jar. - Change
Greeter.ktto declarefun main(args: Array<String>). Have it printHello, $name!wherenameisargs[0]if an argument was passed, or"World"otherwise (hint:args.getOrNull(0) ?: "World"). Run it once with an argument and once without, and confirm both outputs. - Build
Greeter.ktinto a jar without-include-runtime, then try running it withjava -jar. Read the error you get and explain, in your own words, why it happens.
Summary
kotlinccompiles Kotlin source into JVM bytecode —.classfiles, or a.jarwhen a jar target is specified.- A top-level
fun main()is the required entry point; the compiler wraps it in a generated class named after the source file (FileNameKt). - The
kotlincommand runs compiled classes (and.ktsscripts directly), automatically putting the Kotlin runtime on the classpath. -include-runtimebundles the Kotlin standard library into a jar so it can run with plainjavaon any machine.- The REPL and online playgrounds are the fastest way to try a small snippet without creating a file at all.
- In real projects, a build tool like Gradle and your IDE run this exact pipeline for you — knowing the raw commands is what lets you understand build errors and write build scripts yourself.
