Fail
Failure is typed control flow: an error value is thrown with fail, travels up the call chain, and every call site is checked by the compiler for its consumption obligation. The language has no exceptions: failures never happen silently outside the types, and there is no try-catch safety net to fall back on.
The fail Clause and Throwing
Functions that can fail declare their error set with a fail clause, written before the return type (in the order uses, fail, where, return type):
error NotFound(key: string);
func MustLoad(key: string) fail NotFound -> int
{
if key == "gold"
{
return 1;
}
fail new NotFound(key);
}fail expression; throws the error value and leaves the function immediately. The fail set is a static contract: if the errors actually thrown in the body exceed the declared set, the code does not compile.
The Consumption Obligation and Five Forms
Calling a fail function puts the call site under a consumption obligation: every call site must sit in a consuming context, and a missed consumption is a compile-time error whose message lists every fix:
error MS4142: Call to failable function 'Risky' (fail Bad) is not consumed: wrap it in 'try?' / 'try!' / 'capture' / a 'try { ... } recover { ... }' block, or declare 'fail Bad' on 'Sum' (bare 'try' propagates and requires that declaration).There are exactly five consuming forms: try (propagate), try? (null on failure), try! (assert success), capture (collect into a value), and the block form try { } recover { }. This page threads its examples through one failing function (it fails on negatives and doubles positives):
error Bad(code: int, msg: string);
func Risky(a: int) fail Bad -> int
{
if a < 0
{
fail new Bad(a, "negative");
}
return a * 2;
}try: The Propagating Form
try expr does not catch the failure; it passes the failure further up, and the enclosing function must declare an error set covering it:
func Sum(a: int, b: int) fail Bad -> int
{
val x = try Risky(a);
val y = try? Risky(b);
return x + (y ?? 0);
}The failure surface of try Risky(a) enters Sum’s obligation set, which is why Sum declares fail Bad. Propagation is a declarative rethrow: the error value is not rewritten along the way.
try? and try!
try? flattens a failure into null (returning T?), pairing with ?? for defaults; try! asserts success and crashes in place on failure:
print(try! Risky(21));
print(try? Sum(2, 3), try? Sum(-1, 3));42
10 nullInside Sum(-1, 3), try Risky(-1) propagates the failure, Sum as a whole fails, and the outer try? receives null.
Block Form: try { } recover { }
Multiple statements share one consumption point: any fail inside the block flows to the recover, and the block’s final expression is the Success payload:
val v = try
{
val p = Risky(-1);
p + 100
}
recover
{
Success(x) => x,
Failure(e) => e.code,
};
print(v);-1Risky(-1) throws Bad(-1, "negative"), the recover’s Failure(e) arm catches it, and e.code reads the payload field. recover enforces exhaustiveness: both the Success and Failure arms are required, and a missing arm is a compile-time error.
Error-Name Arms: Destructuring the Payload by Position
Failure(e) is the whole-value fallback arm; writing an error-name arm instead destructures the payload by position, and the bound names are visible only inside the arm body:
val named = try
{
val q = Risky(-7);
q + 100
}
recover
{
Success(x) => x,
Failure(Bad(code, m)) => code,
};
print(named);-7Failure(Bad(code, m)) catches only Bad errors and binds the payload by position (code gets -7).
Error-name arms take guards: a mismatched error name does not hit the arm; the ancestor chain counts, so a base error’s arm catches derived values. When the error set holds several errors, write a like-named arm per error for precise routing. Exhaustiveness is tallied against the error set: a missing member arm is fixed by adding it or by writing Failure(_) as the fallback.
capture and the recover Expression
capture expr collects the result into a result<T, E> value that can go into a variable, pass as an argument, and be consumed later. recover r { arms } is a standalone consuming expression:
val r = recover capture Risky(-9)
{
Success(x) => x,
Failure(e) => e.msg,
};
print(r);negativeStore with capture first, then recover as needed; the two-step split suits collect-first, handle-later flows.
Arm-Value Unions: Error Values Used Directly
When every recover arm produces an error value, the whole expression’s type collapses to an error union (displayed as Meh | Bad); payload fields can be read directly, and is / as can discriminate:
error Meh(n: int);
error Bad(code: int, msg: string);
errors LoadError = Meh | Bad;
func Load(k: int) fail LoadError -> int
{
if k == 0 { fail new Meh(k); }
if k < 0 { fail new Bad(k, "negative"); }
return k;
}
func main() -> int
{
val e = recover capture Load(-5)
{
Success(v) => new Meh(v),
Failure(Bad(code, m)) => new Bad(code, m),
Failure(Meh(n)) => new Meh(n),
};
print(e is Bad, e.code);
return 0;
}True -5eholds the unionMeh | Bad:e.codereads directly (a field only needs to exist on at least one member), ande is Baddiscriminates.- The whole-value arm
Failure(err) => errmerges into the union too.
The arms of the block form try { ... } recover { ... } support error-value exits as well: when the block’s final expression evaluates to an error value, that value is delivered as the Success payload, so let the arm pass it straight through rather than wrap it again:
val e = try
{
val v = Load(7);
new Meh(v)
}
recover
{
Success(m) => m,
Failure(err) => err,
};
print(e is Meh, e.n);True 7In the arm, m is already a Meh; writing Success(m) => new Meh(m.n) would pass the error value in as its int field, a type mismatch. Mixed arms (some arms producing normal values, some producing error values) do not collapse into a union: to have both a normal-value fallback and error values carried out, use the capture + recover expression.
main Exiting with an Error
main can declare a fail clause too. An unconsumed failure propagates to the top level, and the program ends with exit code 1:
error Boom();
func main() fail Boom -> int
{
fail new Boom();
}mush: 脚本失败: Main.BoomThe error name is module-qualified: Main.Boom in the output is module plus error name.
Notes
- A failed
try!crashes in place, asserting that the operation must succeed; the semantics connect to Crash; - A consumed error value is ordinary data: printable, reference-comparable, with a readable
@site. Master the five forms before the deep-consumption features (error-name arms, inheritance arms); - A bare
printcannot see the fail channel; a failure value must be converted through a consuming form before it works as an ordinary value; - Struct constructors can declare a fail clause too; the
newexpression is then a fallible call, and its call site must consume.
