Skip to content

Error handling

tim-hardcastle edited this page Sep 9, 2023 · 10 revisions

Error handling

A runtime error is a first-class object. When a function/operation returns an error, it literally returns an error:

→ 1 / 0           
                                                    
[0] Error: division by zero at line 1 of REPL input.

→ 

With a few exceptions (pun unavoidable) when a function/operator is passed an error, it automatically returns that error:

→ 1 / 0                                                               

[0] Error: division by zero at line 1 of REPL input.

→ 1 + 1 / 0                                                           

[0] Error: division by zero at line 1 of REPL input.

string 1 + 1 / 0                                                    

[0] Error: division by zero at line 1 of REPL input.

→ 

An uncaught error will therefore propagate upwards through the calling functions until either it is returned to the REPL, or an attempt is made to assign it to a global variable, in which case the assignment will return the error instead just like any other operator.

You can create your own errors by type conversion from string to error, and they work the same way:

error "this is my error"       
                                     
[0] Error: this is my error at line 1 of REPL input.

1 + error "this is my error"             
                           
[0] Error: this is my error at line 1 of REPL input.

→    

But there are things you can do to stop errors from propagating.

  • You can use the type function to find its type. Instead of returning an error, this returns error.
  • You can assign it to a local constant. (Of course this means you’d be assigning the valid data to the same constant if there was any, so you still have to do something with it.)
  • (Not implemented yet.) You can index it by its fields, e.g. message and line number.

Let’s have a look at examples/error.ch. This shows four possible implementations of a true mod function (where the result is never negative) to supplement the % operator.

def

// We could propagate the error thrown by %.

(x int) modA (y int) :
    remainder >= 0 : remainder
    else : remainder + y
given :
    remainder = x % y

// We could anticipate the error and throw our own.

(x int) modB (y int) :
    y == 0 : error "taking the modulus of a number by zero"
    remainder >= 0 : remainder
    else : remainder + y
given :
    remainder = x % y

// We could catch the error and throw our own.

(x int) modC (y int) :
    type remainder == error : error "taking the modulus of a number by zero"
    remainder >= 0 : remainder
    else : remainder + y
given :
    remainder = x % y

// We could catch the error and return something other than an error.

(x int) modD (y int) :
    type remainder == error : "this isn't an error, just a friendly warning"
    remainder >= 0 : remainder
    else : remainder + y
given :
    remainder = x % y

// ... etc, etc.

The way errors work has one consequence you might not guess, which we should point out as a potential hazard. The rule that a function or operator applied to an error yields that same error applies also to the , operator, the comma.

This means that even in a function which normally returns multiple return values, you can’t return an error and another data object.

If you try to do multiple assignment to local constants, and you assign an error to them, this is legal, and the same error will be assigned to all of them: x, y = error "just the one" is legal and meaningful in the given block of a function.

🧿 Pipefish

Clone this wiki locally