Refingere has no exceptions. An error is an ordinary value, returned like any other result, and a function that can fail says so in its return type. Nothing is thrown past the caller: every error is handled - or explicitly passed on - right where it's returned.
Error ! U8 reads "an error or a U8". The caller can't use the result as a number until it has dealt with the error side, and a function without ! in its type can't fail at all. The error side can be the general Error or a specific kind, such as ParseError ! U64.
error ends the function with an error, the way return ends it with a value. with attaches any value - the input that was wrong, a status code - for whoever handles the error to read back.
Most code doesn't handle an error where it happens; it hands it to its caller. !! does that in two characters: if the value is an error, the function returns it right away, and otherwise !! gives back the value, no longer possibly an error. So a chain of steps that can each fail reads as the happy path, with every point where it can stop still marked.
match takes a result apart. A case starting with Error matches only an error, and every other case only a value - so a bare name like age gets the successful result. The error patterns are:
See Pattern Matching for the rest of what a case can test.
An error stays one wherever it's kept: in an Array, a tuple, a struct's field, or passed to a lambda. Reading it back gives the same error. An Array<ParseError ! U8> holds an age or the error each text gave, and Promise.all over promises that can fail gives every result, errors among them, each in its place.
A value can be missing in two ways. T? may be undefined - a lookup that found nothing, a setting that isn't set - which is an ordinary answer, not a failure. E ! T may be an error: something went wrong, and the error says what. The operators for them come in pairs, ? for undefined and ! for an error, and each handles only its own side:
?? gives a value in place of undefined, !? one in place of an error, and when a value can be both, you write both. The checker reports the wrong one - a ?? on something that can only fail would never do anything. ?! goes the other way, turning a missing value into an error that can be passed up:
When a plain fallback isn't enough, !> runs a function on the error - to wrap it with context, log it, or recover - and ?> does the same for undefined:
And ?|> and !|> pipe a value through a function only when there is one, so a chain of steps needs a single fallback at its end:
The Operators reference has the exact rules for all seven - what each gives, how they combine, and the guarded !. and ?. access forms.
Every error is an Error, with a message and, optionally, a cause - the error it wraps. wrap adds context as an error travels up, so the final message says what was being done while the cause still says why it failed. is and find look through the whole chain, so wrapping an IOError doesn't hide it from code that checks for one. The builtin functions fail with more specific errors - ParseError, IndexError, OverflowError, IOError and others; see the Types reference.
A struct combined with Error is an error type: it has Error's fields and methods, and its own fields on top. A function's type can name it - ParseProblem ! Header - and a match case can take its fields apart, so what went wrong travels as data instead of being squeezed into a message.
defer runs an expression when the block ends, however it ends - including with an error from !! - and @onError makes it run only on an error, to undo work that didn't finish. See Resources and Memory.
An error that reaches the end of @main ends the program and is reported. A test fails when it ends with an error and passes otherwise, so a test checks its results with an if and an error, and with puts what it actually got into the report. The Markdown Parser passes its errors all the way up this way.