Basically, the difference between Lisp conditions and exceptions in mainstream languages is this: the search for a handler is not conflated with unwinding the stack and performing a non-local transfer (the big jump from the throw site to the catch site).

When a condition occurs, the handling executes in a new stack frame, so to speak. The dynamic scope is searched for a handler, which is basically a lambda function associated with the type of the condition. When a handler is found it is called. A handler may "decline" the condition by returning, in which case the search continues. No information is thrown away.

Instead of returning, a handler may perform some kind of non-local transfer. For example, it can perform a "sideways GO" out of the closure to the surrounding function. There exist several ways in Lisp to do a nonlocal jump. The most commonly used one in the context of condition handling is the invocation of a restart.

Restarts, like handlers, also have bindings in the dynamic scope: bindings not between exception types and lambdas, but between symbols and lambdas. They resemble the catch/throw mechanism that is also available in Lisp, except that the functionality is richer. For instance, the Lisp program can query what restarts are available at a given point. Also, restart bindings can be conditionally made invisible. Of course, restarts being functions, they take useful arguments.

A handler written by the application developer typically knows about certain restarts that are expected to be available.

A generic handler can simply enumerate all the restarts and whip them up into a menu that is presented to the user. "This is what happened, and here are the available ways of continuing."

There is a lot of flexibility. For example, a handler can be something that is wrapped at some outer frame, and the set of restarts it interacts with can be inner. So, the inner code can effectively ask a handler for help: "Oops, there is a problem, and I don't know what to do: here are three ways to restart me." The appropriate handler then basically chooses.

This could be used to implement authentication: a condition could indicate "I need a password to continue", and a restart could be offered which takes a password, and another one which aborts the transaction. The handler for the condition in some UI layer way above could provide a dialog for asking for the password, which then invokes the restart to continue with the password.

Conditions are not only for bailing out from errors, but for implementing protocols.

A good analogy is CPU exceptions. When a processor handles an exception, it doesn't begin by unwinding the program. It saves as much of the processor state as possible so the program can be inspected as to what happened, and even restarted. This allows us to implement virtual memory: when a program triggers a page fault exception, the situation can be patched up and the program seamlessly restarted from that same instruction.

Lisp has a handler-case macro which simulates simplified exception handling in some block-structured languages. The handler-case macro writes lambda functions and binds them with condition types. The functions are written in such a way that they implicitly perform a non-local control transfer (such as via the aforementioned "sideways GO" to a label outside of the lambda). This GO is performed even before any of the body code is executed. So the effect is that the stack is unwound when the handler is found. Any frames between that frame and the origin of the condition are discarded, including any restarts that might have been available. With this, it is not possible to implement the scenario of some low level code asking for a password; the code is completely aborted ("I didn't have a password so I failed").