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.

janusconcurrencyasyncstructured-concurrencygorustsystemstechnical

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 }
end
end

Three 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") end
end

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

ProfileSpawnChannelsExecutor
:coreForbiddenForbiddenBlocking only
:service100 tasks implicit1000 channelsCBC-MN cooperative
:cluster10 000 tasks10 000 channelsCBC-MN + actors
:sovereignExplicit CapSpawn requiredExplicitUser 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

PropertyGoRust asyncJanus
Orphan tasks possibleYesPartialCompile error
Lifecycle visible to parentNoManualStructural
Cancellation propagationManual context.ContextManual DropTransitive, automatic
Captured-state ergonomicsTrivial; unsafePainfulTrivial; capability-checked
Resource budgetsNoneNoneCapability-typed
GCYesNoNo

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.