Part 9: Errors

An optional can say that a value is missing, but never why. This part gives a failure a voice. A fallible T ! E is an answer of type T or an error of type E saying what went wrong, and the compiler makes sure no failure is ever dropped without the code saying so. By the end of the part you can write functions that fail precisely, decide at each step whether to handle a failure or pass it on, and tell a failure the caller can fix from a bug that should stop the program.

What you will learn

  • Declaring a fallible with T ! E, or ! E when there is no answer, and failing with fail.
  • Opening an outcome with .Success and .Failure, and why a fallible result cannot be ignored.
  • Recovering with catch — one arm per case, or one else fallback for all.
  • Passing a failure to the caller with ?, and adding context on the way with ? else (e => ...).
  • Designing error types: a variant of cases, or a sum of unrelated errors A | B.
  • A fallible Main, turning absence into an error with ?? fail, and nested fallibles.
  • Panic, Assert and DebugAssert for the situations only a bug can reach.

Which error tool?

Most of this part is choosing the right tool for one moment in the code. The questions run in this order:

flowchart TD
    start(["Something can go wrong"]) --> bug{"Can it happen in a<br/>program with no bugs?"}
    bug -- "no, only a bug" --> stop["Panic, Assert, DebugAssert<br/>9.15–9.16"]
    bug -- "yes" --> why{"Does the caller need<br/>to know why?"}
    why -- "no" --> opt["An optional T?<br/>Part 8"]
    why -- "yes" --> fal["A fallible T ! E, or ! E<br/>9.1–9.3"]
    fal --> who{"Who decides what<br/>the failure means?"}
    who -- "this function" --> reason{"Does the reason<br/>matter here?"}
    reason -- "yes" --> arms["match, or catch with<br/>one arm per case<br/>9.4, 9.6"]
    reason -- "no" --> fallback["catch { else => fallback }<br/>or a deliberate discard<br/>9.5, 9.7"]
    who -- "the caller" --> same{"Does the error already<br/>fit the caller's type?"}
    same -- "yes" --> q["? passes it on<br/>9.8, 9.11"]
    same -- "no, or context to add" --> mapped["? else (e => ...)<br/>9.10"]

Lessons

LessonWhat you will learn
9.1Falliblereturn either a value or an error with T ! E
9.2Failreport a failure with fail
9.3Unit falliblea function that returns nothing but can still fail: ! E
9.4Outcomematch a result as .Success or .Failure
9.5Discardwhy the compiler refuses to let a result be ignored, and how to discard one on purpose
9.6Catchhandle the ways an operation can fail with catch
9.7Catch fallbackturn any failure into a default value with catch { else => ... }
9.8Propagatehand a failure straight to the caller with ? instead of matching it
9.9Error variantdescribe the ways an operation can fail with a variant
9.10Error mappingadd context to an error as it passes through with ? else (e => ...)
9.11Error sumfail in more than one way with an error sum A | B
9.12Fallible mainlet Main itself fail, and see the exit status
9.13Absence to errorturn a missing value into a failure with ?? fail
9.14Nested fallibleresults inside results: T? ! E and (T ! E1) ! E2
9.15Panicstop the program when something impossible happens
9.16Assertcheck an assumption while the program runs

Before you start

Finish Part 8: Optionals first — fallibles are built on the same ideas, and several lessons compare the two side by side. You also need variants from Part 6 and the patterns of Part 7. Each lesson's package is in the Examples repository's Errors/ folder:

cd Examples/Errors/Fallible
rux run

After this part

Part 10: Sum types takes the A | B you met in Error sum and makes it a type of its own, usable anywhere. Before moving on, try the checkpoint project Calculator: it evaluates sums and key-press tapes where every way a calculation can fail is a case of an error variant.

For the rules behind this part, see Error handling and Fatal errors in the Rux Reference, and Panic and Assert in the API reference.