Future + wait
Future is bnlang's representation of an asynchronous value. It's a global builtin — no import needed. Combined with the wait keyword, it lets async code read top-to-bottom like synchronous code.
import "request" as r;
function fetch_user(id) {
var resp = wait r.get("https://api.example.com/users/" + str(id));
return resp.data;
}
function main() {
var user = wait fetch_user(7);
print("hello,", user.name);
}
main().fail(function (e) { print("error:", e); });
A Future is pending, fulfilled with a value, or rejected with an error. It transitions exactly once.
Constructing
Future(executor)
Run executor(resolve, reject) synchronously. Whichever of those is called first determines the Future's outcome.
var f = Future(function (resolve, reject) {
timers.set(0, function () { resolve("from timer"); });
});
If the executor itself throws, the Future rejects with the thrown value.
Future.of(value)
An already-fulfilled Future. If value is itself a Future, the new one adopts its eventual state.
Future.of(7).next(function (v) { print(v); }); // 7
Future.err(error)
An already-rejected Future.
Future.err("nope").fail(function (e) { print(e); }); // nope
Combinators
Future.all(list)
Wait for every input to fulfill, then fulfill with a list of their values in input order. Reject as soon as any input rejects.
var all = Future.all([
r.get(url_a),
r.get(url_b),
r.get(url_c)
]);
all.next(function (resps) { print(resps[0].body, resps[1].body, resps[2].body); });
// or inside an async fn:
var resps = wait Future.all([r.get(url_a), r.get(url_b)]);
Elements that aren't Futures are auto-wrapped with Future.of, so you can mix.
Future.race(list)
Settle with the first input to settle (fulfill or reject). Other pending inputs keep running but their outcomes are ignored.
Future.all_settled(list)
Never rejects. Always fulfills with a list of result maps, one per input:
Future.all_settled([Future.of("a"), Future.err("b")])
.next(function (rs) {
print(rs[0].ok, rs[0].value); // true a
print(rs[1].ok, rs[1].error); // false b
});
Chain methods
| Method | Description |
|---|---|
f.next(on_ok) / f.next(on_ok, on_err) | Run on_ok on fulfillment. Returns a new Future of on_ok's return. If on_err is given, also handles rejection. |
f.fail(on_err) | Run on_err on rejection. |
f.always(fn) | Run fn regardless of outcome. |
f.state | Snapshot string: "pending", "fulfilled", or "rejected". |
Callbacks that throw cause the downstream Future to reject with the thrown value. Callbacks that return a Future cause the downstream to adopt its state — so you can chain async work step by step.
Future.of(10)
.next(function (v) { return v * 2; }) // returns 20
.next(function (v) { return Future.of(v + 1); }) // adopts → 21
.next(function (v) { print(v); }); // 21
wait
wait future_expr suspends the current function until the Future settles, then continues with its value. If the Future rejected, wait throws the rejection value (catchable with try/catch).
Supported positions: top-level statement (var x = wait expr; or wait expr;), inside blocks, if/else, while, for-of, and try/catch. Not yet: sub-expression position, finally, C-style for, top-level of a script body.
Any function whose body contains a wait automatically returns a Future. Callers either wait on it or chain with .next/.fail.
function load() {
try {
var resp = wait r.get(url); // can throw
return resp.body;
} catch (e) {
return "fallback"; // rejection handled
}
}
load().next(function (body) { print(body); });
Bridging callback-style APIs
futurify(fn)
Wrap a function whose last argument is an (err, result) callback so it returns a Future:
function legacy(x, cb) {
timers.set(0, function () { cb(null, x * 100); });
}
var f = futurify(legacy);
f(5).next(function (v) { print(v); }); // 500
// or:
var v = wait f(5);
Most lib stdlib modules with async operations already expose a Future-returning surface (request, io, timers.delay, …), so you rarely need futurify directly — it's for your own callback code or third-party native modules.