Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

When things go wrong

How I think about it

There are two kinds of “wrong”, and I treat them completely differently.

A mistake is fatal — and avoidable. Calling something that doesn’t exist, reaching past the end of a list, dividing by zero: these mean the program itself is wrong. I stop, right there, and say why. I don’t offer a way to carry on regardless, because there’s nothing sensible to carry on to. But every one of them can be avoided by the program: check the length first, check for zero first, spell the name right.

Everything else is a result. The player typed a name nobody has. The save file isn’t there. The shop is out of potions. That’s the world being the world, and a good program expects it. So a function returns those, the same way it returns anything else, and whoever called it looks at what came back.

That’s why I have no try, no catch, no ON ERROR. There isn’t a special second way for things to come out of a function. There’s one way, and the “it didn’t work out” answers travel along it like every other answer.

The rest of this lesson is those two ideas, one at a time.

Mistakes I catch before running

Most mistakes never get as far as running. I read everything first.

FUNCTION area(width, height)
    RETURN width * height
END FUNCTION

PRINT "this line never runs"
PRINT aera(3, 4)

Nothing was printed, not even the first line, because aera isn’t anything. Fix the spelling over there and try again.

Mistakes I can only catch while running

Some can’t be seen until the numbers are real. These stop the program where they happen.

scores = [10, 20, 30]
PRINT "about to go wrong"
PRINT scores(3)
PRINT "this never prints"

Fatal, yes — but look how easy it was to avoid. The program knew how long the list was all along. It just didn’t ask.

scores = [10, 20, 30]
wanted = 3

IF wanted < LEN(scores)
    PRINT scores(wanted)
ELSE
    PRINT "there's no score number", wanted
END IF

parts = 0
IF parts = 0 THEN PRINT "nothing to share out" ELSE PRINT 100 / parts
there's no score number 3
nothing to share out

That’s the deal with mistakes: I won’t let one slide, and you never have to make one.

Results that say “it didn’t work out”

Now the other kind. A function can hand back an error instead of an answer. First it has to declare the error, in its doc comment: a name, what I should say, and optionally a tip. Words in {braces} are filled in when it happens.

/// Finds a player's score.
/// !player-unknown: Nobody called `{name}` is playing!
/// !tip: The players are ada and grace.
FUNCTION score_of(player)
    LOCAL scores = {ada: 12, grace: 30}
    IF scores(player) = NULL THEN RETURN ERROR("player-unknown", {name: player})
    RETURN scores(player)
END FUNCTION

PRINT score_of("ada")

found = score_of("linus")
IF ISERROR(found)
    PRINT "no such player, carrying on"
ELSE
    PRINT found
END IF
12
no such player, carrying on

An error is a value. You can hold it, test it with ISERROR, and decide what to do. All is well: a question was asked, and “nobody by that name” was the honest answer.

A result nobody looked at becomes a mistake

Here’s where the two ideas meet. If an error is used as if it were an ordinary value — printed, added to, passed along — then the program stops, with the message its function declared.

/// Finds a player's score.
/// !player-unknown: Nobody called `{name}` is playing!
/// !tip: The players are ada and grace.
FUNCTION score_of(player)
    LOCAL scores = {ada: 12, grace: 30}
    IF scores(player) = NULL THEN RETURN ERROR("player-unknown", {name: player})
    RETURN scores(player)
END FUNCTION

PRINT score_of("linus") + 10

Not looking at a result that could be “it didn’t work out” is a mistake in the program, so it’s treated like one: fatal, and avoidable. The fix is the ISERROR check from the example before.

So you check the results you can do something about, and anything you didn’t check stops the program by itself, with a proper explanation, at the first place it mattered. Nothing is ever quietly swallowed, and there’s no try to wrap around things.

From Python and JavaScript: No exceptions flying up the stack. An error comes back from the call that made it, and it’s yours to look at. Ignore it and it stops the program at the first place it’s used.

From Lua: Like returning nil, err, except you can’t forget to check: the error is the value.

From BASIC: No ON ERROR GOTO. The check goes right where the thing might fail.

Crash-tan’s Tip: Write the message for the person who’ll be reading it at the worst possible moment. Say what you were looking at, then what to try. That’s what I do!