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).

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.

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("[", ", ", "]")}")
Command line (after --) Output

Pico 12.5 hard hut1 hut2

Pico: 12.5 km, Hard, stops = [hut1, hut2]

Pico 12.5 steep

Illegal command line after 2 arguments: java.lang.IllegalArgumentException: enum Difficulty has no case with name: Steep

Pico

Illegal command line after first argument: more arguments expected

Pico x

Illegal command line after first argument: java.lang.NumberFormatException: For input string: "x"

Pico 12.5

Illegal command line after 2 arguments: more arguments expected

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]   ^^^^^

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).

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

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.

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.