Skip to content

:service — The Workshop

“Where ideas become applications.”

:service is where Janus becomes a proper tool for building real software. You get everything in :core — plus error handling that doesn’t lie to you, async operations that don’t block your brain, and channels that make concurrency actually comprehensible.


No exceptions. No try/catch ambiguity. Errors are values, handled like any other data.

func fetch_url(url: String) !Response do
let response := try http.get(url)
if response.status != 200 do
fail Error.http_error(response.status)
end
return response
end
  • !T — Function returns either T or an error
  • try — Propagate error upward
  • catch — Handle specific errors
  • ? — Short-circuit on error
async func main() do
let result := try await fetch_user(user_id)
print(result.name)
end
  • async — Marks a function as asynchronous
  • await — Pauses until operation completes
  • No callback hell — async code reads like sync code
func worker(input: Channel[String], output: Channel[Result]) do
for msg in input do
output.send(process(msg))
end
end
  • TypedChannel[String] vs. untyped queues
  • Buffered — Optional capacity for backpressure
  • Select — Wait on multiple channels simultaneously
  • using statement for resource cleanup
  • defer for guaranteed teardown
  • Generics with trait constraints
  • First-class error types with rich context

ExcludedAvailable In
Actors and grains:cluster
Supervision trees:cluster
Tensors and GPU:compute
Raw pointers and unsafe:sovereign

Perfect for:

  • Web services and REST APIs
  • Background job processors
  • Database clients and ORMs
  • Microservices
  • Business logic layer
  • Any application that needs to handle errors gracefully
  • Services that communicate over the network

The rule: If it needs to run for more than a few seconds and handle errors, :service is your starting point.


async func handle_request(req: Request) !Response do
let path := req.path
match path do
| "/" => return Response.html(home_page())
| "/api/users" =>
let users := try await db.get_all_users()
return Response.json(users)
| _ => return Response.status(404)
end
end
async func main() do
let server := Server.new(8080)
try await server.serve(handle_request)
end
async func fetch_all(user_ids: [UserId]) ![User] do
let nursery := Nursery.new()
for id in user_ids do
nursery.spawn(async do
return try await db.get_user(id)
end)
end
return try nursery.join_all()
end
func main() do
let jobs := Channel[Job].new(100)
let results := Channel[Result].new(100)
# Start workers
for i in 0..4 do
spawn worker(jobs, results)
end
# Send work
for job in load_jobs() do
jobs.send(job)
end
jobs.close()
# Collect results
for _ in 0..job_count do
let result := results.recv()
print("Job ${result.id}: ${result.status}")
end
end
func process_config(path: String) !Config do
let content := try read_file(path)
| catch FileError.not_found => fail ConfigError.missing(path)
| catch FileError.permission_denied => fail ConfigError.unreadable(path)
let parsed := try TOML.parse(content)
| catch TOMLError => fail ConfigError.invalid(path)
return try Config.from_toml(parsed)
end

vs. Node.js:

  • Types throughout — no undefined is not a function
  • Error handling that’s explicit — no uncaught exceptions
  • Structured concurrency — no callback pyramid

vs. Go:

  • Generics — no code duplication for different types
  • Error handling as values — no if err != nil spam
  • Type safety — Go’s interface{} has no place here

vs. Python (FastAPI/Django):

  • Compile-time type checking — catch bugs before deployment
  • Zero-cost async — no event loop overhead
  • Single language end-to-end — same Janus for frontend, backend, ops


Build applications that matter.