Hello World & @main Methods
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Scala 3.10.0 (released 2026-10-12), run with Scala CLI 1.17.1 on JDK 26 and checked on
2026-10-15. Everything here also works on the 3.9 LTS line and on 3.3 LTS, except two details that are called out where
they appear: extends App is deprecated since 3.8.0, and 3.3 writes a given instance with with. The examples use the trailhead scenario, a toolkit that plans hikes
on a network of mountain trails. Installation is on Installing
Scala; the interactive tools are on
REPL, Worksheets & Scastie; scala run itself is
covered in depth on Scala CLI.
The Scala CLI examples print a few progress lines (Compiling project (Scala 3.10.0, JVM (26))) before the program’s own
output. Those lines are left out of the outputs below.
The first program
Scala 3 turns any top-level method annotated with @main into a program entry point. The method needs no enclosing
class or object, no static, and no String[] args parameter:
//> using scala 3.10.0
@main def hello(): Unit =
println("Hello, trailhead!")
The first line is a Scala CLI using directive: a comment that starts with //> and configures the build, here the Scala
version. Save the file as hello.scala and run it:
scala run hello.scala
Hello, trailhead!
scala run compiles the file in a hidden build directory and runs it. If the directory holds exactly one @main
method, nothing else is needed; with several, the command stops and asks for --main-class (see
Several entry points).
Source: Scala 3 Book: Hello, World!
What the compiler generates
For each @main method the compiler generates a synthetic class with the method’s name and a Java-style
public static void main(String[]) that parses the command line and calls the method. The class goes in the same package as
the method. In the example below the method sits in a package, so the entry point is trailhead.hello:
package trailhead
@main def hello(): Unit =
println("Hello from scalac")
Compiling with scalac (the compiler that ships with the Scala distribution) and listing the output shows the
generated class next to the class that holds the top-level definitions:
scalac -d out Main.scala
find out -type f
out/trailhead/hello.tasty
out/trailhead/hello.class
out/trailhead/Main$package$.class
out/trailhead/Main$package.class
out/trailhead/Main$package.tasty
.tasty files are Scala 3’s serialized typed trees, used by the compiler for separate compilation and by tools; they
are not needed to run the program. Running the class with plain java works as long as the Scala library is on the
class path. In the 3.10.0 distribution the standard library is the single file lib/scala.jar:
java -cp out:$SCALA_HOME/lib/scala.jar trailhead.hello
Hello from scalac
Without scala.jar on the class path the JVM fails with NoClassDefFoundError: scala/util/CommandLineParser$ParseError,
because the generated main refers to that class. A Scala program is always a JVM program that needs the Scala library;
fat jars built by sbt or scala package bundle it.
Source: Scala 3 Book: Main Methods
Command-line arguments
An @main method may declare parameters. The compiler generates the code that reads them from the command line,
converts each one to the declared type and calls the method. A parameter with a default value is optional, and a
repeated parameter, written String*, collects all the remaining arguments:
//> using scala 3.10.0
@main def plan(trail: String, km: Double, days: Int = 1, rest: String*): Unit =
println(s"$trail: $km km in $days day(s)")
println(s"also: ${rest.mkString(", ")}")
Arguments after -- go to the program (the options before it belong to Scala CLI):
scala run plan.scala -- Refugio-Pico 12.5 2 a b
Refugio-Pico: 12.5 km in 2 day(s)
also: a, b
The conversion is done by the type class scala.util.CommandLineParser.FromString. The library provides instances for
String, Boolean, Byte, Short, Int, Long, Float and Double. A program that uses all eight compiles and
runs:
//> using scala 3.10.0
@main def types(a: Boolean, b: Byte, c: Short, d: Int, e: Long, f: Float, g: Double, h: String): Unit =
println((a, b, c, d, e, f, g, h))
(true,1,2,3,4,5.5,6.5,s)
Any other type, for example BigInt or java.io.File, has no instance, and the program does not compile:
//> using scala 3.10.0
@main def nested(p: java.io.File): Unit = println(p)
[error] ./Bad2.scala:2:1
[error] No given instance of type scala.util.CommandLineParser.FromString[java.io.File] was found for parameter fs of method parseArgument in object CommandLineParser
[error] @main def nested(p: java.io.File): Unit = println(p)
[error] ^
The next section shows how to supply the missing instance.
User-defined types as parameters
To accept a user-defined type, define a given FromString instance for it. The instance has one method,
fromString(s: String): A, and any exception it throws is reported as an argument error:
//> using scala 3.10.0
import scala.util.CommandLineParser.FromString
enum Difficulty:
case Easy, Moderate, Hard
given FromString[Difficulty]:
def fromString(s: String): Difficulty = Difficulty.valueOf(s.capitalize)
@main def grade(name: String, difficulty: Difficulty): Unit =
println(s"$name is $difficulty")
scala run grade.scala -- Pico hard
Pico is Hard
given FromString[Difficulty]: followed by an indented body is the Scala 3.6 and later syntax for an anonymous instance
of a type class (Scala 3.0 to 3.5, including the 3.3 LTS, write given FromString[Difficulty] with); the full story is on
Givens & Using Clauses and
Type Classes. An enum with no parameters, as here, is the Scala 3
replacement for Enumeration (see Enums).
Bad arguments
When the command line does not match the signature, the generated code prints an error that says how many arguments were
parsed correctly and why the next one failed, and the method is not called. All of these runs use the trailPlan
program below, which combines the pieces above:
//> using scala 3.10.0
import scala.util.CommandLineParser.FromString
enum Difficulty:
case Easy, Moderate, Hard
given FromString[Difficulty]:
def fromString(s: String): Difficulty = Difficulty.valueOf(s.capitalize)
/** Plans a hike over `km` kilometres, rated `difficulty`. */
@main def trailPlan(name: String, km: Double, difficulty: Difficulty = Difficulty.Easy, stops: String*): Unit =
println(s"$name: $km km, $difficulty, stops = ${stops.mkString("[", ", ", "]")}")
Source: Scala 3 Reference: Main Methods
Command line (after --) |
Output |
|---|---|
|
|
|
|
|
|
|
|
|
|
The last row is a pitfall. difficulty has a default, yet Pico 12.5 is rejected on 3.10.0: when the method also has a
repeated parameter, every argument before the repeated one must be supplied. The same holds for days in the earlier
plan example, which worked only because all of its arguments were given. Avoid combining a defaulted parameter with a
repeated one in an entry point, or drop the repeated parameter.
Two more observations, both from running the programs above. An argument that is converted but never consumed is
ignored: a method with no parameters, or with one, runs normally if extra arguments are passed (running a one-parameter method with the arguments a b prints a). And an argument error does not change the exit status: the process prints the message and exits with
status 0, so scripts that call a Scala program cannot rely on $? to detect bad input unless the program checks for
itself.
A well-formed @main method is restricted: it must be reachable statically. Declared inside a class, it is rejected:
//> using scala 3.10.0
class Tool:
@main def inside(): Unit = println("x")
[error] ./Bad.scala:3:3
[error] method inside cannot be a main method since it cannot be accessed statically
[error] @main def inside(): Unit = println("x")
[error] ^^^^^
Source: Scala 3 Reference: Main Methods
An @main method defined inside a top-level object is accepted, because the object makes it accessible statically;
it runs as scala run Obj.scala and prints in object.
Several entry points
A file or project can define any number of @main methods, each one with its own generated class. scala run refuses
to guess:
[error] Found several main classes: Settings, run3
You can run one of them by passing it with the --main-class option, e.g.
scala run App3.scala --main-class Settings
You can pick the main class interactively by passing the --interactive option.
scala run App3.scala --interactive
--main-class <name> selects one, and --main-class-list (short form --main-class-ls) lists the candidates. With java, the class name is the argument
itself (java -cp ... trailhead.hello).
Source: Scala 3 Reference: Main Methods
The main method form
The Java-style entry point still works unchanged in Scala 3. It is a method called main that takes an
Array[String] and sits in an object; because an object is a singleton, its methods are already static from the
JVM’s point of view:
//> using scala 3.10.0
object Legacy:
def main(args: Array[String]): Unit =
println(s"args: ${args.toList}")
scala run Legacy.scala -- a b
args: List(a, b)
This is the only form in Scala 2 and the one that Scala 2 books and most older code use. In Scala 3 it is still the
right choice when the entry point must be a particular class, for example in a manifest, in a build tool setting that
names a main class, or in code that must also compile with Scala 2. @main is Scala 3 only. The tour shows the same
comparison:
A Tour of Scala: Basics, program entry point.
Compared with Java, Scala needs no public static void, the Unit return type replaces void, and an object replaces
the class that exists only to hold main. Compared with Kotlin, @main is the counterpart of a top-level
fun main(args: Array<String>) (see Kotlin), with the difference that a
Scala @main method may declare typed parameters and gets the parsing for free. In Java the corresponding
tool is public static void main(String[] args) (see Java); Groovy
scripts need neither (see Running Scripts
& the groovy Command).
Why extends App is discouraged
Scala 2 offered a shortcut for programs: an object that extends the App trait, whose body is the program:
//> using scala 3.10.0
object Greeter extends App:
println("start")
It still compiles and runs on Scala 3.10, but since Scala 3.8.0 it carries a deprecation warning, shown here with
-deprecation:
[warn] ./App1.scala:3:24
[warn] trait App in package scala is deprecated since 3.8.0: Support for trait App is deprecated in Scala 3. Please refer to https://docs.scala-lang.org/scala3/book/methods-main-methods.html.
[warn] object Greeter extends App:
[warn] ^^^
start
The reason is in DelayedInit. In Scala 2, App extended DelayedInit, a marker trait that made the compiler move the
object body into a method that main ran later. That is a special-case compiler transformation, and Scala 3 dropped
it: there is no DelayedInit, so the body of an object extends App is ordinary object initialization. The effect,
observed on 3.10.0 with a body that prints and a field:
//> using scala 3.10.0
//> using option -nowarn
object Settings extends App:
println("body runs")
val limitKm = 20
@main def run5(): Unit =
println("main starts")
println(Settings.limitKm)
println("calling Settings.main")
Settings.main(Array.empty)
main starts
body runs
20
calling Settings.main
The body ran the first time Settings was touched (when limitKm was read), not when Settings.main was called; the
call to main at the end did nothing visible. Run directly with --main-class Settings, the program prints body runs
once. Scala 2 code that relied on the delayed initialization, for example code that reads a field of the object before
main has run and expects a default value, behaves differently. Unlike @main, extends App gives the program only an
untyped args array, with no parsing and no error messages.
New code should use @main, or a plain object with a main method when a Scala 2 compatible shape is required.
Scripts and shebang lines
A file with the extension .sc is a script: its top-level statements run in order, with no @main method and no
object. The arguments are available as args:
//> using scala 3.10.0
val names = List("Pico", "Refugio", "Ridge")
println(s"args = ${args.toList}")
println(names.map(_.length).sum)
scala run trails.sc -- one two
args = List(one, two)
16
Source: Scala CLI: Scripts, Scala CLI: Shebang
Behind the scenes Scala CLI wraps the script in an object and compiles it like any other source. Scripts are meant for
small automation, experiments and examples; larger programs belong in .scala files.
On Unix-like systems a script can run as an executable if it starts with a shebang line that names the shebang
subcommand of the launcher. scala shebang is like scala run, tuned for that use:
#!/usr/bin/env -S scala shebang
println("shebang ok")
chmod +x sb.sc
./sb.sc
shebang ok
The -S option of env splits the rest of the line into separate arguments. It is needed because many systems, Linux
among them, pass everything after the interpreter name as one argument, so the line #!/usr/bin/env scala shebang
would look for a program called scala shebang (the example above was run on macOS only). Scala CLI’s own documentation uses
scala-cli shebang in its examples, which is the same command under its original name.
Source: Scala CLI: Scripts, Scala CLI: Shebang
Where the books are outdated
The Scala 3 Book and the Scala Reference document @main. Books written for Scala 2, and Scala 3 books that predate
the 3.8 changes, typically present extends App or def main(args: Array[String]) as the normal way to start a
program and use sbt run, or scalac followed by scala Hello, as the only workflow. Three things have moved on:
@main is the idiomatic Scala 3 entry point; extends App is deprecated since 3.8.0; and the scala command of the
3.10.0 distribution is Scala CLI (version 1.17.1 here), so scala run compiles and runs sources in one step, selects
the entry point with --main-class, and runs scripts and shebang files.