chapter 04

failure is a value

kanso has no exceptions and no panics, and no wrapper types such as Ok and Some to unwrap. a success is the bare value. an absence is none, an ordinary value. a failure is an err, which is born in a box: it passes every function it reaches until a rescue in another module takes it out, or it reaches the top of the program and is reported. this chapter is mostly about choosing between the two. none is the outcome a caller handles. an err is the failure nobody in the module that raised it may handle, and that rule is what keeps a program's error handling small.

start by breaking something:

ratio.kso
import "std/text"

word = text/trim " many "
text/to_int! word .> (ratio -> print "ratio: {ratio}")
kanso run ratio.kso
error[endpoint]: unhandled err reached the entry: ""many" is not an integer"
  born in the entry at ratio.kso:4

the program did not crash. to_int! is the conversion for a caller who insists the text is a number. it could not read "many" as one, so it answered a failure. the failure went past the .> step without running it and reached the top of the program, where the endpoint rule applies: an err that reaches the top of the program is reported as an error. nothing was thrown and no stack was unwound. the second line of the report is carried by the err itself: its birthplace, the line that asked for the number.

drop the ! and the same conversion asks instead of insisting. text/to_int answers none for text that is not a number, and the program prints what it got:

asked.kso
import "std/text"

word = text/trim " many "
ratio = text/to_int word
print "ratio: {ratio}"
kanso run asked.kso
ratio: <none>

every conversion in the standard library that can refuse comes in this pair. the plain form answers an outcome for the caller to handle, and the ! form answers a failure for nobody to handle. we come back to none below.

the railway

a failure passes through functions. when one arrives as an argument, the function body does not run: the failure goes past unchanged and keeps moving. here is a failure born two calls deep, riding all the way out:

railway.kso
total = subtotal 120 0
print (receipt total)

fn receipt total
  "order total: {total}"

fn subtotal cents people
  with_tip (share_of cents people)

fn share_of _ 0
  err "no one to share the bill with"

fn share_of cents people
  cents / people

fn with_tip share
  share + share / 10
kanso play railway.kso
error[endpoint]: unhandled err reached the entry: "no one to share the bill with"
  born in share_of at railway.kso:11
  passed through receipt ← with_tip

the program has no error handling in it. share_of has nobody to divide the bill among and says so with an err. the err passes with_tip, which does not run, then reaches the top of the program, passes receipt and print, and is reported. this is the railway: failures run on a track beside the happy path, so each function states its happy path and nothing else.

one rule sits beside this, and the checker enforces it. a failure is a box, and a box is not a value. where the checker can prove an expression is only ever a failure, it refuses to let an operator, an index or an interpolation read it, the same way it refuses an io (chapter 05 has those): err "closed" + 1 does not compile. share_of can answer a number, so the checker lets the call through and the endpoint reports the failure when one comes. when a step should run only on a value, .> says so: share_of cents people .> with_tip calls with_tip with a share and steps past a failure.

the err built its report as it travelled. the first line is the reason. the second is the birthplace: function, file and line, recorded when the failure was made. the third is the trace, every function the err passed, newest on the left, so receipt ← with_tip means it passed with_tip first and receipt after. since a failure never unwinds a stack, the trace is how the path it took is kept: on the value itself, printed only if the err reaches the top.

nothing is asked of the signature

readers who know checked exceptions or result types usually ask the same question here. if a failure can arrive in an argument, does the function's signature have to say so? if one of five arguments is a failure, must the function now return one? it does not. a failure that reaches a call ends the call before the body runs, so the function never receives the failure, and there is nothing to say about it in a signature. failure in, failure out is a rule about calls.

unasked.kso
board = { "soup":450 }
soup = price board "soup"
flan = price board "flan"
print (label "soup" soup) .> (_ -> print (label "flan" flan))

fn price menu dish
  listed menu[dish] dish

fn listed none dish
  err "{dish} is not on the board"

fn listed cents _
  cents

fn label dish cents
  "{dish}: {cents} cents"
kanso play unasked.kso
soup: 450 cents
error[endpoint]: unhandled err reached the executor: "flan is not on the board"
  born in listed at unasked.kso:10
  passed through label

label takes two arguments. on the first line both are values and it runs. on the second, one of them is a failure, because price looked for flan and the board has none. the call answers the failure and the body never starts, which is what passed through label in the trace reports. one function, compiled once, serves both lines, and nothing about label had to change to allow the second. there is no annotation to add, no wrapper type to return and no ? operator. the work other languages put in every signature is done at each call by the rule itself.

when more than one argument fails, a call keeps the first failure; an operator keeps both, because neither operand caused the other. the full accounting of what each construct does with an arriving failure—which merge, which take the first, which hold, which pass through—is a pinned table on the compiler page, and every row of it is a fixture that runs on all three engines.

no arm takes it

chapter 03 taught you that dispatch is the only switch, so the first question is what an err does when it reaches a function with an arm for everything. it rides past the whole overload group, including the catch-all arm that answers for everything else in the language:

unhandled.kso
fn describe none
  "nothing"

fn describe x
  "got {x}"

fn steeps "green"
  3

fn steeps leaf
  err "{leaf} is not a tea we steep"

print (describe (steeps "barley"))
kanso play unhandled.kso
error[endpoint]: unhandled err reached the entry: "barley is not a tea we steep"
  born in steeps at unhandled.kso:11
  passed through describe

describe x answers for ints, strings, records, markers—anything. it still never sees the err: passed through describe, says the trace. arms read values, and a failure is in a box, so no arm takes it: not a catch-all, and not an (err _) arm either. an argument the function never reads is never computed, so an err that would have come from it never happens. a failure ends one of two ways. a rescue in another package takes it out of its box, or the endpoint reports it.

taking a failure out of its box has a word, and chapter 05 introduces it properly: rescue, usually written .?. the word is licensed on foreign failures only. the same .? aimed at your own err does nothing at all, and one program shows both. teahouse raises; kitchen imports it, raises one of its own, and hands both to the same callback:

teahouse/teahouse.kso
pub fn brew _
  err "the kettle is cold"
kitchen/kitchen.kso
import "../teahouse"

fn apologise (err _)
  "sorry, no tea"

pub fn served _
  teahouse/brew "sencha" .? apologise

fn scorched _
  err "i burnt it myself"

pub fn mine _
  scorched "sencha" .? apologise
boundary.kso
import "./kitchen"

print (kitchen/served "go") .> (_ -> print (kitchen/mine "go"))

served answers sorry, no tea. mine reaches the endpoint, and the report never mentions the callback:

kanso run boundary.kso
sorry, no tea
error[endpoint]: unhandled err reached the executor: "i burnt it myself"
  born in kitchen/scorched at kitchen/kitchen.kso:10

one callback, two .? steps, opposite answers. an err carries where it was born, and a .? written in the module that raised it hands the failure on without entering the callback, which is why apologise is absent from the report. what a rescue hands its callback is the err out of its box, and out of its box an err is an ordinary value: apologise names it with an (err _) arm, and it could as well store it, pass it along, or put it back in a box with effect. inside your own module there is nothing to rescue, because a failure you raise is one you decided nobody should handle.

with catchable exceptions, a failure goes to whichever handler catches it first, and you find out which one that is at run time. in kanso you decide where a failure goes by the kind of value you return. a failure callers should cope with travels as a value: none, a marker, or a record that names the verdict. a failure nobody should cope with is an err, and only a .? in another module can end it. so what a caller can handle is a value, and what it cannot is an err.

none is for asking

the standard library is consistent about where you meet none. indexing with the sigil, xs[i]!, is strict: it means "i know this element exists." the read answers an effect, a box holding the element, and .> opens it. a miss is a broken promise, a failure that reaches the top through the box:

ninth.kso
flavors = ["matcha" "ube" "hojicha"]
flavors[9]! .> (f -> print "ninth: {f}")
kanso run ninth.kso
error[endpoint]: unhandled err reached the entry: "missing index 9"
  born in the entry at ninth.kso:2

(indexing is 1-based, everywhere, no exceptions. the first flavor is flavors[1]. we are at peace with this.)

when absence is expected—you're asking, not promising—drop the sigil: plain xs[i] returns none on a miss. and none, unlike err, is a shape dispatch fully understands. it sits on the marker rung of the specificity ladder, so handling absence is one more arm, written like any other:

fourth.kso
fn describe none
  "no fourth flavor"

fn describe x
  "fourth flavor: {x}"

flavors = ["matcha" "ube" "hojicha"]
first = flavors[1]
fourth = flavors[4]
print "first: {first}" .> (_ -> print (describe fourth))
output
first: matcha
no fourth flavor

the same pair of tools covers maps—literal syntax { "dango":350 }, strict lookup with m[k]!, polite lookup with plain m[k]:

menu.kso
fn describe none
  "not on the menu"

fn describe price
  "{price} yen"

menu = { "dango":350 "taiyaki":500 }
print (describe menu["pocky"])
output
not on the menu

handleable does not mean invisible. a none no arm ever claims does not melt into the output as a plausible blank—it renders as <none>, a sentinel that is loud on purpose:

ignored.kso
flavors = ["matcha" "ube"]
third = flavors[3]
print "third: {third}"
kanso run ignored.kso
third: <none>

the plain subscript asked a question, nothing answered it, and the output shows that: <none> is absence printed where a value should be. (a none that is the program's final value, rather than part of a string, still trips the endpoint rule; appendix a has that diagnostic.) so both kinds of value show up. the difference is that an arm can claim a none and no arm can claim an err. asking for something you might not get leaves two honest endings: an arm that claims the none, or a <none> in the output.

access is a decision

so the same missing key can be a failure that rises or a none you dispatch on, and you choose at each call site with one character. xs[i]! states an invariant: this key exists, and if it does not, that is a bug, so fail here. xs[i] asks a question: this may be absent, and my code handles absence. you cannot assume by accident, and you cannot cope by accident:

pocky.kso
menu = { "dango":350 "taiyaki":500 }
menu["pocky"]! .> (yen -> print "pocky: {yen}")
kanso run pocky.kso
error[endpoint]: unhandled err reached the entry: "missing index "pocky""
  born in the entry at pocky.kso:2

same map, same missing key as menu.kso, but this call site claimed pocky was on the menu, so the miss is a broken promise and the report says so. what other languages leave to comments and code review is written in the call.

the reasoning behind this is "you aren't gonna need it", applied to errors: by default, do not handle a failure. a handler for a failure that never happens is code paid for up front against something that may never come. so kanso's default is to assume, let the err rise with its report, and add handling one call site at a time when a real failure shows it is needed. the endpoint rule makes sure an unplanned failure is reported loudly the day it happens.

designing the sad path

the same line—handle, or rise—runs through every function you design, because you now have two different tools to fail with. the question that picks between them: did the function accomplish its goal, or not?

a parser that reads "banana" and decides it is not a flag has not failed. it was asked a question about some text and it produced the answer: no. that verdict is an accomplishment—a value—and it should ship as one, because callers will want to dispatch on it:

flag.kso
type malformed

fn describe malformed
  "not a flag"

fn describe b
  "flag: {b}"

fn parse_flag "off"
  false

fn parse_flag "on"
  true

fn parse_flag _
  malformed

print (describe (parse_flag "on"))
  .> (_ -> print (describe (parse_flag "banana")))
output
flag: true
not a flag

malformed is a marker from chapter 03 doing failure work: it names the verdict, it dispatches like anything else, and no caller can forget it exists—an unclaimed marker rides to the endpoint just like an unclaimed none. this is the shape to reach for whenever bad input is an answer your callers care about.

minting an err is the opposite declaration: my goal is unmet and i decline to make that my caller's chore. err takes a reason and builds the failure by hand, and the natural place to write one is an arm that recognizes a state you refuse to continue from. here a none arrives where absence means the input itself is broken, and the function escalates it:

escalate.kso
import "std/text"

fn first_major lines
  major_of lines[1]

fn major_of none
  err "no version line"

fn major_of line
  text/to_int line

print "major: {first_major []}"
kanso play escalate.kso
error[endpoint]: unhandled err reached the entry: "no version line"
  born in major_of at escalate.kso:7

the conversion went one way: a none became an err, because major_of decided that a missing version line is not something anyone downstream should cope with. inside the module that raised it, nothing turns that err back into a none. so choose with care: none and markers when callers should decide, an err when there is nothing sensible for them to decide. a caller can end your err only by rescuing it from another module, so making one is a commitment: the failure will appear under your function's name in their report, with your reason. write the reason for the stranger who will read it there.

the two failures

every failure so far was born in pure code, and that fact is visible right on the report: a birthplace, file and line. the taxonomy underneath deserves to be explicit, because it decides what fixing a failure even means. the next programs read a file, which makes them a small preview of chapter 05—read_file hands the executor a description of the read, and .> feeds the result to a function. that description is a value with a type of its own, <string>effect, which is what chapter 05 spends its length on. take the plumbing on credit for two pages; the failures are today's subject.

a file named version.txt holds the text 7:

version.kso
import "std/os"
import "std/text"

fn announce text
  major_of text .> (n -> print "major version {n}")

fn major_of text
  text/to_int! text

os/read_file! "version.txt" .> announce
output
major version 7

now the same program pointed at garbage.txt, whose content is the text seven point four:

garbage.kso
import "std/os"
import "std/text"

fn announce text
  major_of text .> (n -> print "major version {n}")

fn major_of text
  text/to_int! text

os/read_file! "garbage.txt" .> announce
kanso play garbage.kso
error[endpoint]: unhandled err reached the executor: ""seven point four" is not an integer"
  born in major_of at garbage.kso:8

and once more, pointed at a file that does not exist:

missing.kso
import "std/os"
import "std/text"

fn announce text
  major_of text .> (n -> print "major version {n}")

fn major_of text
  text/to_int! text

os/read_file! "no-such-file.txt" .> announce
kanso play missing.kso
error[endpoint]: unhandled err reached the executor: "cannot read no-such-file.txt: no such file"
  born in the entry at missing.kso:10

two failures from the same program shape, and each report names where its err was born. neither report says which kind of failure it is, and the kinds differ. the garbage failure is a wrong: text/to_int!, called from major_of, turned a value into a verdict in pure code. to_int! "seven point four" equals that err the way 2 + 2 equals 4, and running it again computes the same err at the same line, every time. the missing-file failure is a not-yet: an io that could not complete, out in the world. no line of your code is wrong, and the world may have moved by the next attempt. to tell them apart, ask whether the step that failed touched the world.

a common objection: the garbage came from a file, and files can change, so isn't that failure contingent too? follow it step by step. the read succeeded: it delivered seven point four intact, which is why execution reached major_of. the parse then failed in code that never touched the disk. a failure's kind is fixed by where it is born. the part that can change is one step upstream, at the read, and reading again is a new event, not a retry of the parse. that is why retrying belongs at the edge of the program: retrying a not-yet asks the world a question it may answer differently, and retrying a wrong asks the same arithmetic to come out different. the runtime already marks every err with where it was born, which is what a retry vocabulary for effects will rest on.

failures in company

one question remains: what happens when two failures happen at once? plain consecutive statements have no order between them, and a bind, .>, is what puts one step after another; chapter 06 is about that rule. an err arriving at a bind skips everything after it, exactly as the railway predicts. but between statements with no order there is no "after," so neither failure can preempt the other. both sides run, and both failures arrive:

teas.kso
import "std/text"

sencha = text/trim " one "
hojicha = text/trim " two "
text/to_int! sencha .> (n -> print "sencha: {n} steeps left")
text/to_int! hojicha .> (n -> print "hojicha: {n} steeps left")
kanso run teas.kso
error[endpoint]: unhandled err reached the entry: [""one" is not an integer" ""two" is not an integer"]

the two errs merge into one whose reason is the list of both. a merged err trades the single birthplace line for that list—two failures, two birthplaces, one report—which is the honest accounting: neither failure caused the other, so neither deserves top billing. sequence the same two lines and the story collapses back to the railway—first failure wins, and the second print never runs:

seq.kso
import "std/text"

sencha = text/trim " one "
hojicha = text/trim " two "
text/to_int! sencha .> (n -> print "sencha: {n} steeps left")
  .> (_ -> text/to_int! hojicha .> (n -> print "hojicha: {n} steeps left"))
kanso run seq.kso
error[endpoint]: unhandled err reached the entry: ""one" is not an integer"
  born in the entry at seq.kso:5

so where there is an order, the first failure stops what follows, and where there is none, failures accumulate. you do not choose a policy for this. the structure of the program decides it.

the compiler cannot always see the dependence. these two statements are adjacent, so they are a group, so they run together:

grouped.kso
import "std/os"

fn show p
  print "read: [{p.stdout}]"

os/run "sh" ["-c" "sleep 1; printf written > slow.txt"]
os/run "cat" ["slow.txt"] .> show
kanso play grouped.kso
read: []

the write takes a second. the read does not wait for it, because nothing tells it to—no value passes from the first statement to the second. the dependency between them runs through the filesystem, which is outside the program, and no analysis can follow it there. the read finishes first and finds nothing.

a bind says what the compiler could not work out:

ordered.kso
import "std/os"

fn show p
  print "read: [{p.stdout}]"

os/run "sh" ["-c" "sleep 1; printf written > slow.txt"]
  .> (_ -> os/run "cat" ["slow.txt"] .> show)
kanso play ordered.kso
read: [written]

this is the edge of "dependence decides." the compiler orders what it can prove is ordered, and the rest is yours to say. when two statements touch the same file, the same socket, the same directory, you are the only one who knows.

err - always arrives, never uninvited
err

people mix me up with my cousin none. none is what you get when you asked, and i am what you get when something broke. i am born in a box, and no arm opens a box: i walk past a catch-all and past an arm written for me alike. only a stranger's .? can take me out, and what it takes out is a plain value; the module that raised me hands me on. if you ever wish you could catch me some other way, you wished for the wrong value: ship a none, or a marker that names the verdict. i exist for the failures you refused to plan for, and i always file a full report on the way out.

you now have the whole failure model. an err is for what broke: born in a box, it passes every arm with its birthplace and trace until a rescue in another module takes it out or the top of the program reports it. none is for what was absent: one more shape, claimed by one more arm, and impossible to ignore silently. each access chooses between promising and asking. your own functions choose between a verdict returned as a value and a failure made with err. a failure born in pure code is a wrong, and one born at the edge is a not-yet. and whether failures stop the next step or accumulate follows from whether there is an order between them. chapter 05 turns to the world outside the program: if a failure is a value, so is an effect.

exercises

  1. add a fourth station to railway.kso—a function between subtotal and with_tip—and write down, before running, the exact report you expect: reason, birthplace, and the full passed-through chain in order. then run it and grade yourself.
  2. escalate.kso turns a none into an err. redesign it the other way: make major_of ship a marker (say unversioned) instead of minting an err, and dispatch on it at the print site. write one sentence on which caller the failure now belongs to, and whether that is an improvement here.
  3. take fourth.kso and flip both access decisions—make the first lookup a question and the fourth a promise. predict what each run prints or reports before checking. for each of the two call sites, state in one sentence which meaning the original program actually wanted and why.
  4. run version.kso against three worlds: the file holding 7, the file holding garbage, and the file absent. before each run, name the failure kind you expect—wrong or not-yet—and say whether the report will carry a birthplace line. explain what retrying would mean, if anything, in each world.