Concurrent Ceylon CLI checks with futures and async
Run independent host checks concurrently in Ceylon using future, async, and run, with bounds on fan-out and exception handling at await.
30 Jan 2026, 07:30 UTC

A slow sequential check in a Ceylon CLI tool
Imagine a command-line tool that verifies whether a list of hosts is reachable. The straightforward implementation calls a blocking network check for each host, one after another:
shared void run() {
value hosts = ["host-one.example", "host-two.example", "host-three.example"];
for (host in hosts) {
print(checkHost(host));
}
}
If each check takes two seconds, the tool needs six seconds. The CPU is idle most of that time, waiting on the network.
What future, async, and run do
A future is a handle to a result that is not ready yet. The async keyword marks a routine as suspendable: the Ceylon runtime schedules its body on a worker pool and returns a future immediately. await retrieves the result, suspending the current routine until the future completes. The module entry point run can return a future; when it does, the runtime starts that top-level future automatically instead of treating run as a plain blocking entry point.
This matters because it lets you express concurrency without creating threads yourself. The trade-off is that you must reason about how many futures are in flight and where exceptions surface.
Worked example: launch checks concurrently
The following is illustrative Ceylon; confirm exact syntax against your compiler version before adopting it.
shared Future<Nothing> run() async {
value hosts = ["host-one.example", "host-two.example", "host-three.example"];
value checks = hosts.map((host) => async => checkHost(host));
for (check in checks) {
try {
value result = await check;
print(result);
} catch (Exception e) {
print("check failed for a host: " + e.message);
}
}
}
Here run returns Future<Nothing>, so the runtime starts it asynchronously. Each async => checkHost(host) produces a future. The loop awaits each one, printing results as they complete and catching exceptions at the point of await.
Where to run: a normal development terminal, as an unprivileged user. Compile with ceylon compile <module>, then run with ceylon run <module>. No sudo is required unless you write artifacts to a system directory.
Expected check: total wall-clock time should move closer to the slowest single check rather than the sum of all checks, and output order may vary between runs. Risk: if hosts is large, you create one future per host with no limit on concurrency.
Limitation: unbounded fan-out and deferred exceptions
Creating many futures without bounding concurrency can saturate the default worker pool, raising latency or causing task rejection. Exceptions inside an async routine do not throw when the future is created; they throw when you await it. An unwrapped await can therefore crash the module or hide a failure in a log you never inspect.
Practical way to check the result: run the tool with time ceylon run <module> for a small host list, then for a larger list. If runtime grows linearly instead of plateauing, introduce chunked processing or a concurrency cap before deploying. Inspect your Ceylon toolchain documentation for the default worker pool size; it is not safe to assume it matches your workload.
Closing
Use future and async when independent I/O dominates your command-line tool's runtime. Start with a small, fixed set of concurrent operations, wrap every await in try, and verify entry-point behavior with your installed Ceylon compiler. If latency worsens after adding concurrency, revert the run change and measure again.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.