The Idea in One Sentence
Zero programs do I/O by being given permission to, not by reaching for an ambient global.
That permission is a value of type World. The runtime constructs one before calling main, and your program threads it (or pieces of it) wherever it needs to interact with the outside world.
Why No Globals?
Most languages let any function, anywhere, write to stdout or open a file. JavaScript has console.log. Python has print. C has printf. The convenience is real, but so is the cost: you can't tell from a function's signature whether it might do I/O. To know, you have to read the body — recursively.
Zero takes a different stance. There is no global print. There is no ambient os.Stdout. If your function does I/O, that fact has to show up in its signature, because the only way to do I/O is to have been handed a capability.
The benefits show up in three places:
- Reading a signature tells you what a function can do. A function that doesn't mention
Worldcan't write to stdout, can't open a socket, can't read a file. The type system makes that a hard guarantee. - Testing pure code is trivial. Pure functions don't need stub
printor mock file systems — they don't have access to them in the first place. - Agents can reason locally. An AI agent generating or repairing Zero code can know — without reading the whole codebase — whether a function it's looking at has effects.
The Canonical Use
You've already seen the basic shape from hello-world:
Three things going on:
maindeclares its parameter asworld: World. The runtime hands the program aWorldvalue and binds it here.world.outis the standard-output stream, exposed as a field of theWorldcapability.world.out.write(...)writes a string. It returns a fallible value (the write could fail), whichcheckpropagates.
You can also rename the parameter — w: World or io: World — but world is the convention and worth keeping for consistency with the broader Zero ecosystem.
What Lives on World
The World capability exposes the surfaces a program might need. The exact shape depends on what the runtime supports, but you can expect entries for:
world.out— standard output.world.err— standard error.world.in— standard input.- A way to open files, read environment variables, and connect over the network.
Refer to the current Zero standard-library docs for the authoritative list of fields. Some surfaces (network, filesystem) may live behind narrower capability types accessible through World rather than at the top level.
Threading Capabilities Through Code
The catch — the price you pay for explicit effects — is that any function that needs to do I/O has to receive the capability. You can't reach for it implicitly.
fun log(world: World, message: String) -> Void raises {
check world.out.write(message)
}
pub fun main(world: World) -> Void raises {
log(world, "starting\n")
log(world, "done\n")
}
log takes world so it can write through it. If log didn't take world, the body couldn't call world.out.write — the binding wouldn't exist.
This is more parameter plumbing than in a language with ambient I/O. The trade is that the entire call graph for main is now visible from signatures alone:
maintakesworld, so it might do I/O.logtakesworld, so it might do I/O.- Any function without
worldin its signature can't.
Narrower Capabilities
Passing the whole World to every function is a blunt approach — it's like handing out root privileges. The pattern Zero encourages is to take only the slice of World you actually need:
fun log(out: Stream, message: String) -> Void raises {
check out.write(message)
}
pub fun main(world: World) -> Void raises {
log(world.out, "starting\n")
log(world.out, "done\n")
}
Now log only gets access to a Stream (the same type world.out exposes). It can write through it but it can't open a file or read from the network. The caller chose what log is allowed to do.
The exact type names you'll see in real Zero code (Stream, Writer, capability slices) will track the standard library's vocabulary in your toolchain version. The pattern — pass the minimum, not the maximum — is universal.
Pure Functions
Functions that don't need World should not take World. This is partly a style preference and partly enforced by the type system: there's no way to do I/O without a capability.
fun sum(point: Point) -> i32 {
return point.x + point.y
}
sum is pure with respect to the outside world. A caller looking at the signature knows for certain that this function won't print anything, won't open a file, won't ping a server. That's a property a static analyzer (or an agent) can rely on without reading the body.
Capabilities and raises
Almost every operation on a capability is fallible. world.out.write can fail because the stream is closed. A file open can fail because the file doesn't exist. The capability API surface is paired with raises and check — fallible operations declare their failure modes in their signatures, and callers acknowledge them with check.
The combination is the heart of Zero's effect story:
- What can happen →
raises { ... }. - Through what →
World(or a slice of it). - Where → wherever the capability and
raisesare visible.
That's enough information to reason precisely about a function's effects from its signature alone.
A Note on Testing
Capability-based I/O makes testing simple by construction. Want to capture the output of a function under test? Pass it a fake out capability that records what it was asked to write. Want to test a function that's supposed to be pure? Don't pass it any capability — its type signature won't let it touch the outside world.
The standard library can provide test harnesses that build fake or in-memory capabilities for exactly this purpose. The exact API will evolve with the language; the principle (capabilities are values you can substitute) is the lever.
Next: Raises and Check
World is half the effect story — the surfaces a function can touch. The other half is failure: when something goes wrong, how does it propagate? That's covered next in Raises and Check.
Frequently Asked Questions
What is the World in Zero?
World is the runtime-provided capability object that grants a Zero program access to the outside world: stdout, stdin, files, the network, environment variables, and so on. The runtime constructs a World value and passes it to main. Functions that need to do I/O have to be passed the World (or a narrower slice of it) — there's no global escape hatch.
Why does main take a World parameter?
Zero has no ambient globals. There's no equivalent of printf, console.log, or os.Stdout that any function can call without permission. The runtime hands main a World capability and main (and any function it calls) can only do I/O through that value. This makes every effect visible in a function's signature.
How is capability-based I/O different from normal I/O?
In most languages, I/O is implicit — any function can write to stdout or read from the filesystem at any time. Capability-based I/O makes the permission to do I/O a value: you have to be given a World (or a slice of one) to use it. Pure computation functions don't take World and so literally cannot do I/O, which the type system enforces.
Can I get the World implicitly somewhere deep in my call stack?
No, by design. If a helper deep in the call stack needs to write to stdout, you have to pass it the World — or a narrower capability — explicitly. That's more parameter plumbing than in a language with ambient I/O, but it's the cost of being able to read a function's signature and know whether it might do I/O.
What does world.out.write do?
world.out.write("text\n") writes the given string to the program's standard-output stream through the capability the runtime provided. It returns a fallible value — the write might fail — so you wrap the call with check to propagate the error up the call stack.