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

Errors are values

throw takes any value. there is no error type and no condition hierarchy:

narju> (attempt (lambda () (throw 3)))
('throw . 3)

narju> (attempt (lambda () 3))
('ok . 3)

attempt tags because a thrown 3 and a returned 3 are the same value.

the built-in failures are pairs of a symbol and whatever detail there was:

narju> (attempt (lambda () (car 'nope)))
('throw 'wrong-type . 'car)

narju> (attempt (lambda () (assert (< 2 1) 'ordering)))
('throw 'assert-failed . 'ordering)

narju> (attempt (lambda () (check 'sum (+ 1 1) 3)))
('throw 'check-failed 'sum 2 3)

matching on one is tagged? and cdr:

(let ((r (attempt (lambda () (throw '(no-such-key . b))))))
  (if (tagged? r 'throw) (cdr r) 'fine))
('no-such-key . 'b)

Two arms

a continuation is ('cont f . h): what to do with a value, and what to do with what a raise carried.

(define (cont f h) (cons 'cont (cons f h)))
(define (cont-in k f) (cont f (cont-raise k)))

cont-in inherits the raise arm unchanged, so the arms behave like a stack without one existing. a raise walks the object-level continuation chain as an ordinary value.

if the floor unwound instead, a protected region would be delimited by floor frames and a computation inside one could not suspend. since a task suspends on every RPC, that would make attempt and call mutually exclusive:

(define attempt
  (lambda reflect (m l e r k)
    (meta m l 'base-eval (car e) r
          (cont-in k
                   (lambda (th)
                     (apply l th '()
                            (cont (lambda (v) (apply-cont k (cons 'ok v)))
                                  (lambda (v) (apply-cont k (cons 'throw v))))))))))

both arms continue into k, so attempt protects without delimiting. prompt delimits without protecting:

narju> (attempt (lambda () (prompt (lambda () (throw 'through)))))
('throw . 'through)

a continuation captured outside an attempt still works from inside one:

(say (call/cc (lambda (k)
  (attempt (lambda () (+ 1 (k 'escaped)))))))
'escaped

Uncaught

a raise nobody catches ends the task. in a script that is the exit:

(say 'before)
(throw '(gave-up 7))
'before
naj: task 0 failed: ('throw 'gave-up 7)
$ echo $?
1

at the prompt it is shown and the session continues, because naj/repl.naj wraps each line in attempt and prints ('error . v). a decision the repl made, not a language rule.

Across a message

a task that refuses a request sends ('throw . why) back, and call re-raises it in the caller, so an RPC failure is caught like a local one:

narju> (attempt (lambda () (call t '(get b))))
('throw 'no-such-key . 'b)
(define (refuse req why) (reply req (cons 'throw why)))

what a task does when a peer dies is a different question, there being no continuation to raise into. see Supervision.