Why Janus Concurrency Doesn't Leak
Go made concurrency easy to start and impossible to own. Rust made it correct and too often miserable to compose. Janus took the third door: structured concurrency by construction.
Why Janus Concurrency Doesn’t Leak
Go made concurrency easy to start and impossible to own. Rust made concurrency correct and too often miserable to compose. Janus took the third door:
Structured concurrency by construction; enforced at parse time.
That is the whole move.
TL;DR
Go normalized orphaned concurrency. Rust made the correct async story costly to compose. Janus makes structured concurrency the law of the land.
No task exists outside a nursery. No nursery exits with live children. No runtime shrug. No fire-and-forget graveyard. No ambient leak culture disguised as ergonomics.
The easy thing to write is the correct thing to ship.
The Go Pain, Diagnosed
go func() { ... } is the original sin.
You fire a goroutine into the void and the runtime shrugs. No parent. No required handle. No structural lifecycle. No guaranteed error path. No obligation to join. The thing can outlive its caller, outlive the request, outlive the intention, and now the cleanup story is “hope the team is disciplined.”
That is not a tooling problem. It is not a documentation problem. It is not a skill issue.
It is a language-design problem.
Go made ergonomic the wrong thing: spawning without ownership.
The Primagen complaints in that clip are not just entertainment. They are a symptom of a concurrency model that made the dangerous thing feel normal.
The Rust Pain, Diagnosed
Rust did not make the opposite mistake. It made a different one.
The ownership model is precious in synchronous code. In async code, the tax arrives. Capturing state in a task, sharing a channel across futures, moving something just far enough to placate the compiler without setting the whole file on fire; the semantics are often obvious, but the proof burden is still yours.
The model is right. The ergonomics are uneven.
Rust makes the correct thing possible. Too often, it does not make it pleasant.
What Janus Does
Law: No task exists outside a nursery. No nursery exits with live children.
In Janus, the nursery is not a library convention. It is not a best-practice blog post. It is not something the senior engineers remember while the juniors improvise around it.
It is the language shape.
spawn outside a nursery do ... end is a compile error. There is no detach(). No fire-and-forget. No runtime shrug. No “we’ll clean it up later” concurrency.
async func scrape(urls: Array[string]) -> Array[Page] !NetworkError do nursery do let handles = urls.map { |url| spawn fetch_page(url) } return handles.map { |h| await h } endendThree guarantees fall out of the syntax itself:
- No orphans. The parent cannot exit while children live.
- First-error-wins. If one sibling fails, the nursery cancels the rest.
- Cooperative cancellation. Children choose how to unwind; they do not choose whether they are subject to shutdown.
This is what Go should have done.
Channels Are the Comms; Not Shared Mutable State
Law: Inside a nursery, tasks talk through
Channel[T]. Period.
This is the CSP move Go got right and then failed to finish structurally.
Channel[T] is built in. select is built in. Cancellation participates directly. Closed channels behave predictably. Buffered values drain before closure becomes an error. No interpretive dance around half-dead pipelines.
select do case msg = inbox.recv() do process(msg) end case timeout(5000) do log("idle") endendIf the parent dies, the waiting child wakes with cancellation. That is not a convention. That is the contract.
Boring. Predictable. Correct.
Exactly what concurrency should be.
Capabilities Cap the Damage
The part both Go and Rust are missing: a budget.
Janus profiles put a hard ceiling on damage.
| Profile | Spawn | Channels | Executor |
|---|---|---|---|
:core | Forbidden | Forbidden | Blocking only |
:service | 100 tasks implicit | 1000 channels | CBC-MN cooperative |
:cluster | 10 000 tasks | 10 000 channels | CBC-MN + actors |
:sovereign | Explicit CapSpawn required | Explicit | User choice |
You do not accidentally fork-bomb a :service binary. The language says no. The capability says no. The budget says no.
That is what a serious system language should do: not merely expose power, but bound it.
The Scheduler, Briefly
CBC-MN: Capability-Budgeted Cooperative M:N.
Stackful fibers. Work-stealing. Fast spawn. Fast switching. Budget-aware execution.
In plain English: Go’s M:N upside without the same philosophical sloppiness; Rust’s performance seriousness without forcing stackless-lifetime acrobatics into every async conversation.
The runtime matters. The language decision matters more.
The scheduler is powerful because the language refuses to generate garbage concurrency in the first place.
The Scoreboard
| Property | Go | Rust async | Janus |
|---|---|---|---|
| Orphan tasks possible | Yes | Partial | Compile error |
| Lifecycle visible to parent | No | Manual | Structural |
| Cancellation propagation | Manual context.Context | Manual Drop | Transitive, automatic |
| Captured-state ergonomics | Trivial; unsafe | Painful | Trivial; capability-checked |
| Resource budgets | None | None | Capability-typed |
| GC | Yes | No | No |
That is the asymmetry.
Go made the easy thing leak.
Rust made the correct thing hurt.
Janus makes the correct thing easy.
Why This Matters
Concurrency is where languages reveal whether they respect production reality or merely demo well.
Go optimized for launch velocity and normalized orphan work. Rust optimized for semantic correctness and charged interest on composition. Janus starts from a different premise:
If concurrency can escape structure, it eventually becomes sabotage.
So we removed the escape hatch.
The result is simple to state and rare to find: the thing developers want to write under pressure is also the thing you can trust in production.
That is not a patch on Go.
That is not a prettier Rust executor.
That is a better concurrency bargain.
And yes; holy shit, finally.