1. vararg as part of signature design
When you first see vararg, it can seem like it’s just “a parameter you can pass many times.” But at the API level, it’s a strong design choice: it changes the style of calling a function and forces the compiler to decide where the “many arguments” end and where the “settings” begin.
That’s why it’s important to design the signature so it’s easy both to read and to call—especially for a beginner who still can’t “guess the author’s intent” from a function.
Technically, Kotlin says two key things: inside the function, vararg is treated as an array, and vararg is usually placed last; if it is not last, then everything that comes after it must be provided by name at the call site.
vararg inside a function: think of it as an array
To confidently read signatures, it helps to “ground the magic”: inside the function body, vararg is a regular array of elements of the corresponding type. That means you can read size, access by index xs[0] (if it’s not empty), iterate with for (x in xs), and so on.
Let’s make a small “microscope”—a function that shows what we actually received:
fun debugInts(vararg xs: Int) {
println("count = ${xs.size}") // count = ...
if (xs.size > 0) {
println("first = ${xs[0]}") // first = ...
}
}
fun main() {
debugInts(10, 20, 30) // count = 3, first = 10
}
There is no secret collection or special container here: xs is exactly what’s convenient to iterate over as an array.
Why vararg is usually placed last
There is a simple rule from “readable code design”: when a person sees the call f(…, …, …), they should be able to understand at a glance what the “tail” of arguments means.
vararg is logically almost always such a tail: “pass a list of messages,” “pass a set of numbers,” “pass strings to print.” That’s why most of the time vararg is placed at the end of the signature—the call looks natural, and the compiler doesn’t have to guess where the list ends.
In Kotlin this is also practical: if vararg is last, you can pass values just by listing them, and no extra requirements on the call style appear.
An example that “reads well”: first one required parameter, then vararg.
fun log(tag: String, vararg messages: String) {
for (m in messages) {
println("[$tag] $m")
}
}
fun main() {
log("AUTH", "start", "check password", "ok")
// [AUTH] start
// [AUTH] check password
// [AUTH] ok
}
Restriction: only one vararg per function
A signature must be unambiguous. If Kotlin allowed two vararg parameters, then the call f(1, 2, 3) would be a mystery: what belongs to the first vararg, and what belongs to the second?
That’s why Kotlin allows exactly one vararg per function. This is a direct language rule.
If you suddenly wanted to write this:
// Not allowed: two vararg parameters in one function
// fun broken(vararg a: Int, vararg b: Int) { }
Then the compiler will rightly say “no.”
2. vararg not at the end: named args and default values
Why you almost always need named arguments after vararg
Sometimes you want a signature like “first the list, then the settings.” For example: “print the elements, but with this separator.” And then the temptation appears to write fun printAll(vararg items: String, separator: String).
This is allowed, but Kotlin warns us: if vararg is not last, then values for parameters after it must be passed by name, otherwise the compiler cannot tell where items ends and where separator begins.
Let’s make an example using a small utility that we’ll use in our learning console app (let’s call it GuessGame, “Guess the number”). We often want to print a list of strings on one line.
fun printAll(vararg items: String, separator: String) {
var first = true
var out = ""
for (item in items) {
if (!first) out += separator
out += item
first = false
}
println(out)
}
fun main() {
printAll("a", "b", "c", separator = " | ") // a | b | c
}
Notice: we passed separator only as separator = " | ". If you try to do printAll("a", "b", "c", " | "), the compiler won’t accept it, because it looks like “one more vararg element.”
vararg and default values
A parameter after vararg is often an “option,” and it usually has a reasonable default value. Then the signature becomes friendlier: you can omit the option in simple cases, and specify it only when needed.
However, because the parameter comes after vararg, in real calls you will almost always use named arguments, even if the parameter has a default value. This is not “mandatory bureaucracy,” but a way to make the call readable: it’s easier for a person to see separator = " | " than to guess what the last string means.
Let’s make our printAll utility a bit more convenient:
fun printAll(vararg items: String, separator: String = ", ") {
var first = true
var out = ""
for (item in items) {
if (!first) out += separator
out += item
first = false
}
println(out)
}
fun main() {
printAll("kotlin", "java", "swift") // kotlin, java, swift
printAll("kotlin", "java", "swift", separator = " / ")
// kotlin / java / swift
}
Here’s the key point: a separator parameter with a default improves the signature because the typical call stays short. But as soon as we want to change separator, we still write the parameter name explicitly—this makes the code more readable, and we don’t fight the compiler.
Rule: “skip one—name the rest”
Now we get to the part that surprises beginners most often: “Why can’t I just skip one argument in the middle but pass the rest positionally?” Because positional calling relies on order, and once you create a “hole,” the compiler needs to know what exactly you skipped.
Kotlin has a rule: you can skip arguments with default values, but after the first skipped one all subsequent arguments must be named.
Let’s show this with a small example that looks like a real “output configuration” in our console app:
fun showAttempt(
attempt: Int,
guess: Int,
hint: String = "no hint",
prefix: String = "Attempt"
) {
println("$prefix #$attempt: guess=$guess, hint=$hint")
}
fun main() {
showAttempt(1, 50) // Attempt #1: guess=50, hint=no hint
showAttempt(2, 75, prefix = "Try") // Try #2: guess=75, hint=no hint
}
Note: in the second call we want to change prefix, but we don’t want to change hint. We “skip” hint, which means we must pass prefix by name (prefix = "Try"), otherwise it would be ambiguous.
3. Parameter order cheat sheet and an example
Parameter order: a mini cheat sheet
Now let’s bring everything together into one picture—not as “rules for the sake of rules,” but as ways to make a function call predictable.
Below is a table of two typical signature forms. It doesn’t replace understanding, but it helps a lot when you design an API and decide where to place vararg.
| Signature | How it reads | How it’s usually called | Where there are fewer surprises |
|---|---|---|---|
|
“first required, then a list” | |
Usually here |
|
“a list, then settings” | |
Only if you really need it |
The idea is this: vararg at the end is almost always the most “user-friendly” design. If you place vararg not at the end, you should be prepared that users of your function (including you a week later) will be writing named arguments after vararg.
Example from a learning app: a logging function
To avoid leaving the topic “in a vacuum,” let’s add a small logger to our console app. We’re not doing anything “industrial”—we need just one thing: print several messages with the same tag. This fits vararg perfectly.
We’ll choose the most user-friendly signature: first the tag, then vararg. This makes the call read naturally.
fun log(tag: String, vararg messages: String) {
for (m in messages) {
println("[$tag] $m")
}
}
fun main() {
log("GAME", "start", "secret generated", "waiting for input")
}
If later we want to add a “setting” (for example, enable/disable logging), we will most likely add a normal parameter before vararg or provide a default so that the call remains readable. At this stage of the course, the main thing is to feel that parameter order is part of your code’s design.
4. Typical mistakes
Mistake #1: trying to pass a parameter after vararg positionally.
Typically it looks like this: you wrote fun f(vararg xs: String, separator: String) and then instinctively call f("a", "b", " | "). But the compiler is not required to guess that the last string is a “setting” rather than yet another list element. In Kotlin, if vararg is not last, parameters after it must be passed by name.
Mistake #2: putting vararg not last “just because,” and then being surprised that all calls became bulky.
Sometimes it seems “nice” to put settings at the end, but the cost is constant named arguments and noisier code. If the parameter after vararg does not provide truly important readability (or doesn’t appear in every other call), it’s better to restructure the signature so that vararg is last.
Mistake #3: forgetting that vararg can be empty, and accessing xs[0] without a check.
With vararg, the call f() is legal, and it’s not a compilation error. Therefore any code that accesses xs[0] must define the contract in advance: either carefully check xs.size > 0, or forbid an empty list via require. Otherwise, you’ll get an error at runtime.
Mistake #4: skipping a parameter with a default value, but continuing to pass the following ones positionally.
This is a common “reading logic” mistake: you want to write something like showAttempt(2, 75, "Try"), meaning prefix, but positionally the third argument is hint. Kotlin deliberately pushes you toward explicitness: if you skipped one default parameter, use named arguments afterward.
Mistake #5: trying to add a second vararg “just in case.”
Even if you could imagine “how it should work,” the compiler cannot unambiguously distribute arguments between two variable-length parameter lists. That’s why Kotlin’s rule of one vararg per function is a strict restriction—and that’s fine.
GO TO FULL VERSION