Skip to content

Commit

Permalink
Minor condition system improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
phoe authored and vindarel committed Jan 9, 2025
1 parent d2000f2 commit ffed607
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 52 deletions.
3 changes: 2 additions & 1 deletion emacs-ide.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,8 @@ You can also use the usual Emacs shortcut from [compilation-mode]( https://www.g
If you don't want to see the red annotations in your source… use `C-c
M-c`, `slime-remove-notes`. They are not automagically fixed though.

Only style warnings may not be caught by the slime-compilation buffer.
If your code has only style warnings, they will be caught by the slime-compilation
buffer, but the buffer will not pop up on its own.

You can find all these keybindings, as usual, under Emac's Slime menu.

Expand Down
91 changes: 55 additions & 36 deletions error_handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ What is a condition ?

Let's dive into it step by step. More resources are given afterwards.

## Throwing/catching versus signaling/handling

Common Lisp has a notion of throwing and catching, but it refers to a different concept
than throwing and catching in C++ or Java.
In Common Lisp,
[`throw`](https://www.lispworks.com/documentation/HyperSpec/Body/s_throw.htm)
and [`catch`](https://www.lispworks.com/documentation/HyperSpec/Body/s_catch.htm)
(like in [Ruby](https://ruby-doc.com/docs/ProgrammingRuby/html/tut_exceptions.html#S4)!)
are a mechanism for transfers of control; they do not refer to working with conditions.

In Common Lisp, conditions are *signaled* and the process of executing code in response
to a signaled condition is called *handling*. Unlike in Java or C++, handling
a condition does not mean that the stack is immediately unwound - it is up to
the individual handler functions to decide if and in what situations
the stack should be unwound.

## Ignoring all errors, returning nil

Expand Down Expand Up @@ -56,18 +71,18 @@ could not choose what to return.
Remember that we can `inspect` the condition with a right click in Slime.


## Catching any condition (handler-case)
## Handling all error conditions (handler-case)

<!-- we will say "handling" for handler-bind -->

`ignore-errors` is built from [handler-case][handler-case]. We can write the previous
example by catching the general `error` but now we can return whatever
example by handling the general `error` but now we can return whatever
we want:

~~~lisp
(handler-case (/ 3 0)
(error (c)
(format t "We caught a condition.~&")
(format t "We handled an error.~&")
(values 0 c)))
; in: HANDLER-CASE (/ 3 0)
; (/ 3 0)
Expand All @@ -78,7 +93,7 @@ we want:
;
; compilation unit finished
; caught 1 STYLE-WARNING condition
We caught a condition.
We handled an error.
0
#<DIVISION-BY-ZERO {1004846AE3}>
~~~
Expand All @@ -95,35 +110,34 @@ The general form of `handler-case` is
...))
~~~


## Catching a specific condition
## Handling a specific condition

We can specify what condition to handle:

~~~lisp
(handler-case (/ 3 0)
(division-by-zero (c)
(format t "Caught division by zero: ~a~%" c)))
(format t "Got division by zero: ~a~%" c)))
;; …
;; Caught division by zero: arithmetic error DIVISION-BY-ZERO signalled
;; Got division by zero: arithmetic error DIVISION-BY-ZERO signalled
;; Operation was (/ 3 0).
;; NIL
~~~

This workflow is similar to a try/catch as found in other languages, but we can do more.

This is the mechanism that is the most similar to the "usual" exception handling
as known from other languages: `throw`/`try`/`catch` from C++ and Java,
`raise`/`try`/`except` from Python, `raise`/`begin`/`rescue` in Ruby,
and so on. But we can do more.

## handler-case VS handler-bind

`handler-case` is similar to the `try/catch` forms that we find in
other languages.

[handler-bind][handler-bind] (see the next examples), is what to use
when we need absolute control over what happens when a signal is
raised. It allows us to use the debugger and restarts, either
[handler-bind][handler-bind] (see the next examples) is what to use
when we need absolute control over what happens when a condition is
signaled. It allows us to use the debugger and restarts, either
interactively or programmatically.

If some library doesn't catch all conditions and lets some bubble out
If some library doesn't handle all conditions and lets some bubble out
to us, we can see the restarts (established by `restart-case`)
anywhere deep in the stack, including restarts established by other
libraries that this library called. And *we can see the stack
Expand Down Expand Up @@ -158,7 +172,7 @@ It's better if we give more information to it when we create a condition, so let
(:documentation "Custom error when we encounter a division by zero.")) ;; good practice ;)
~~~

Now when we'll "signal" or "throw" the condition in our code we'll be
Now, when we signal the condition in our code, we'll be
able to populate it with information to be consumed later:

~~~lisp
Expand Down Expand Up @@ -198,31 +212,35 @@ not standard objects.
A difference is that we can't use `slot-value` on slots.


## Signaling (throwing) conditions: error, warn, signal
## Signaling conditions: error, cerror, warn, signal

We can use [error][error] in two ways:

- `(error "some text")`: signals a condition of type [simple-error][simple-error], and opens-up the interactive debugger.
- `(error 'my-error :message "We did this and that and it didn't work.")`: creates and throws a custom condition with its slot "message" and opens-up the interactive debugger.
- `(error "some text")`: signals a condition of type [simple-error][simple-error],.
- `(error 'my-error :message "We did this and that and it didn't work.")`: creates and signals a custom condition with a value provided for the `message` slot.

In both cases, if the condition is not handled, `error` opens up the interactive debugger, where
the user may select a restart to continue execution.

With our own condition we can do:
With our own condition type from above, we can do:

~~~lisp
(error 'my-division-by-zero :dividend 3)
;; which is a shortcut for
(error (make-condition 'my-division-by-zero :dividend 3))
~~~

Throwing these conditions will enter the interactive debugger, where
the user may select a restart.
`cerror` is like `error`, but automatically establishes a `continue` restart that the user can use to continue execution. It accepts a string as its first argument - this string will be used as the user-visible report for that restart.

`warn` will not enter the debugger (create warning conditions by subclassing [simple-warning][simple-warning]).
`warn` will not enter the debugger (create warning conditions by subclassing [warning][warning]) - if its condition is unhandled, it will log the warning to error output instead.

Use [signal][signal] if you do not want to enter the debugger, but you still want to signal to the upper levels that something *exceptional* happened.
Use [signal][signal] if you do not want to do any printing or enter the debugger, but you still want to signal to the upper levels that some sort of noticeable situation has occurred.

And that can be anything. For example, it can be used to track
progress during an operation. You would create a condition with a
`percent` slot, signal one when progress is made, and the
That situation can be anything, from passing information during normal
operation of your code to grave situations like errors.
For example, it can be used to track progress during an operation.
You can create a condition with a
`percent` slot, signal one whenever progress is made, and the
higher level code would handle it and display it to the user. See the
resources below for more.

Expand All @@ -236,7 +254,7 @@ The class precedence list of `simple-warning` is `simple-warning, simple-condit
### Custom error messages (:report)


So far, when throwing our error, we saw this default text in the
So far, when signaling our error, we saw this default text in the
debugger:

```
Expand Down Expand Up @@ -420,7 +438,7 @@ use the regular `read`.
:interactive (lambda () (prompt-new-value "Please enter a new divisor: "))
;;
;; and call the divide function with the new value…
;; … possibly catching bad input again!
;; … possibly handling bad input again!
(divide-with-restarts x value))))
(defun prompt-new-value (prompt)
Expand Down Expand Up @@ -476,7 +494,7 @@ as if the error didn't occur, as seen in the stack.

### Calling restarts programmatically (handler-bind, invoke-restart)

We have a piece of code that we know can throw conditions. Here,
We have a piece of code that we know can signal conditions. Here,
`divide-with-restarts` can signal an error about a division by
zero. What we want to do, is our higher-level code to automatically
handle it and call the appropriate restart.
Expand All @@ -487,9 +505,9 @@ We can do this with `handler-bind` and [invoke-restart][invoke-restart]:
(defun divide-and-handle-error (x y)
(handler-bind
((division-by-zero (lambda (c)
(format t "Got error: ~a~%" c) ;; error-message
(format t "and will divide by 1~&")
(invoke-restart 'divide-by-one))))
(format t "Got error: ~a~%" c) ;; error-message
(format t "and will divide by 1~&")
(invoke-restart 'divide-by-one))))
(divide-with-restarts x y)))
(divide-and-handle-error 3 0)
Expand Down Expand Up @@ -570,7 +588,7 @@ condition's reader `(opts:option condition)`.

## Running some code, condition or not ("finally") (unwind-protect)

The "finally" part of others `try/catch/finally` forms is done with [unwind-protect][unwind-protect].
The "finally" part of others `try`/`catch`/`finally` forms is done with [unwind-protect][unwind-protect].

It is the construct used in "with-" macros, like `with-open-file`,
which always closes the file after it.
Expand Down Expand Up @@ -605,7 +623,8 @@ You're now more than ready to write some code and to dive into other resources!
* [Algebraic effects - You can touch this !](http://jacek.zlydach.pl/blog/2019-07-24-algebraic-effects-you-can-touch-this.html) - how to use conditions and restarts to implement progress reporting and aborting of a long-running calculation, possibly in an interactive or GUI context.
* [A tutorial on conditions and restarts](https://github.com/stylewarning/lisp-random/blob/master/talks/4may19/root.lisp), based around computing the roots of a real function. It was presented by the author at a Bay Area Julia meetup on may 2019 ([talk slides here](https://github.com/stylewarning/talks/blob/master/4may19-julia-meetup/Bay%20Area%20Julia%20Users%20Meetup%20-%204%20May%202019.pdf)).
* [lisper.in](https://lisper.in/restarts#signaling-validation-errors) - example with parsing a csv file and using restarts with success, [in a flight travel company](https://www.reddit.com/r/lisp/comments/7k85sf/a_tutorial_on_conditions_and_restarts/drceozm/).
* [https://github.com/svetlyak40wt/python-cl-conditions](https://github.com/svetlyak40wt/python-cl-conditions) - implementation of the CL conditions system in Python.
* [https://github.com/svetlyak40wt/python-cl-conditions](https://github.com/svetlyak40wt/python-cl-conditions) - implementation of the CL condition system in Python.
* [https://github.com/phoe/portable-condition-system](https://github.com/phoe/portable-condition-system) - portable implementation of the CL condition system in Common Lisp.

[ignore-errors]: http://www.lispworks.com/documentation/HyperSpec/Body/m_ignore.htm
[handler-case]: http://www.lispworks.com/documentation/HyperSpec/Body/m_hand_1.htm
Expand Down
4 changes: 3 additions & 1 deletion files.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,9 @@ always recommendable to use the macro
instead. Not only will this macro open the file for you and close it when you're
done, it'll also take care of it if your code leaves the body abnormally (such
as by a use of
[throw](http://www.lispworks.com/documentation/HyperSpec/Body/s_throw.htm)). A
[`go`](https://www.lispworks.com/documentation/HyperSpec/Body/s_go.htm),
[`return-from`](https://www.lispworks.com/documentation/HyperSpec/Body/s_ret_fr.htm),
or [`throw`](http://www.lispworks.com/documentation/HyperSpec/Body/s_throw.htm)). A
typical use of `with-open-file` looks like this:

~~~lisp
Expand Down
2 changes: 1 addition & 1 deletion gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ And voilà. Run it with
##### Custom events

We'll implement the same functionality as above, but for demonstration
purposes we'll create our own signal named `name-set` to throw when
purposes we'll create our own signal named `name-set` to get emitted when
the button is clicked.

We start by defining the signal, which happens inside the
Expand Down
2 changes: 1 addition & 1 deletion numbers.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ this results in an error condition:
; Evaluation aborted on #<FLOATING-POINT-OVERFLOW {10041720B3}>.
~~~

The error can be caught and handled, or this behaviour can be
The error can be handled, or this behaviour can be
changed, to return `+infinity`. In SBCL this is:

~~~lisp
Expand Down
4 changes: 2 additions & 2 deletions testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ Before diving into tests, here is a brief introduction of the available checks y

There is also:

* `finishes`: passes if the assertion body executes to normal completion. In other words if body does signal, return-from or throw, then this test fails.
* `pass`: just make the test pass.
* `finishes`: passes if the assertion body executes to normal completion. In other words, if the body signals an error or makes a non-local jump, then this test fails.
* `pass`: marks the test as passed.
* `is-true`: like `is`, but unlike it this check does not inspect the assertion body to determine how to report the failure. Similarly, there is `is-false`.

Please note that all the checks accept an optional reason, as string, that can be formatted with format directives (see more below). When omitted, FiveAM generates a report that explains the failure according to the arguments passed to the function.
Expand Down
4 changes: 2 additions & 2 deletions type.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ november 2019, now gives those warnings, meaning that this:
((name :type number :initform "17")))
~~~

throws a warning at compile time.
signals a warning at compile time.


Note: see also [sanity-clause][sanity-clause], a data
Expand Down Expand Up @@ -530,7 +530,7 @@ string:
(format nil "finally doing sth with ~a" res)))
~~~

Compiling this function doesn't throw a type warning.
Compiling this function doesn't signal a type warning.

However, if we had the problematic line at the function's boundary
we'd get the warning:
Expand Down
7 changes: 3 additions & 4 deletions web-scraping.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,11 @@ that case. If all goes well, we return the return code, that should be
200.

As we saw at the beginning, `dex:get` returns many values, including
the return code. We'll catch only this one with `nth-value` (instead
the return code. We'll access only this one with `nth-value` (instead
of all of them with `multiple-value-bind`) and we'll use
`ignore-errors`, that returns nil in case of an error. We could also
use `handler-case` and catch specific error types (see examples in
dexador's documentation) or (better yet ?) use `handler-bind` to catch
any `condition`.
use `handler-case` and handle specific error types (see examples in
dexador's documentation).

(*ignore-errors has the caveat that when there's an error, we can not
return the element it comes from. We'll get to our ends though.*)
Expand Down
9 changes: 5 additions & 4 deletions web.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ self-contained executable, stability, good threads story, strong typing, etc. We
can, say, define a new route and try it right away, there is no need to restart
any running server. We can change and compile *one function at a time* (the
usual `C-c C-c` in Slime) and try it. The feedback is immediate. We can choose
the degree of interactivity: the web server can catch exceptions and fire the
interactive debugger, or print lisp backtraces on the browser, or display a 404
the degree of interactivity: the web server can refuse to handle exceptions
and fire the interactive debugger instead,
or handle them and print lisp backtraces on the browser, or display a 404
error page and print logs on standard output. The ability to build
self-contained executables eases deployment tremendously (compared to, for
example, npm-based apps), in that we just copy the executable to a server and
Expand Down Expand Up @@ -493,14 +494,14 @@ Then you can parse this string to JSON with the library of your choice ([jzon](h

In all frameworks, we can choose the level of interactivity. The web
framework can return a 404 page and print output on the repl, it can
catch errors and invoke the interactive lisp debugger, or it can show
invoke the interactive lisp debugger, or it can handle the error and show
the lisp backtrace on the html page.

### Hunchentoot

The global variables to set to choose the error handling behaviour are:

- `*catch-errors-p*`: set to `nil` if you want errors to be caught in
- `*catch-errors-p*`: set to `nil` if you want unhandled errors to invoke
the interactive debugger (for development only, of course):

~~~lisp
Expand Down

0 comments on commit ffed607

Please sign in to comment.