Skip to end of metadata
Go to start of metadata

Problems

  • platform uses types for dispatch
    • only error handling is platform error handling
  • want to carry data payloads
    • platform exceptions don't
  • need out-of-band communication
    • for both ordinary and exceptional things!
      • exceptional things are ordinary during development
    • and ways to discover the oob mechanisms
  • can't deal with exception at point it happens
    • can't resume
  • cannot manipulate exceptions as data

Symptoms

  • new application problems beget new types
    • types are heavy
    • why don’t people use an existing type?
      • want to branch on it (flow control)
      • habit
      • carry data
  • new exception types lead to
    • gen-class (+ precompilation)
    • Java classes in Clojure projects
  • use big things to solve small problems
    • condition library (e.g. Contrib Condition) when all you want is data
    • exceptions when all you want is control flow
      • idiomatic on many JVM languages
      • don't have composite returns or dynamic binding
  • can't troubleshoot problems
    • info is gone when call stack unwinds

Example Use Cases

  • communicate ordinary things
    • recover from I/O failure
    • convey detailed information for reporting in test framework
  • communicate exceptional things
    • program bugs
    • platform and hardware failures
  • jump to debugger based on situation
  • add information to an exception
    • e.g. Alan's adorning macro arity example
  • troubleshoot build failure that is only on Hudson

Constraints

  • dispatch should be fast
    • where exceptions are used for exceptional things, performance is not an issue
  • error handling should unify at bottom with Java
  • neither ordinary nor exceptional things should have to pollute API for fns that don't care or can't respond
    • pay for what you need

An even-more modest proposal

I think we should implement

  • education on dynamic bindings
  • bindable *assertion-handler*
  • (maybe) clj-stacktrace-like data-ification fns for exceptions
  • (maybe) data-carrying *RuntimeException* subclass

All of these ideas, and several others not chosen, are documented below.

Some approaches

Table of some approaches and how they address the problems above. Approaches described in more detail below. Some of the approaches are deliberately crazy for contrast (wonder if we will agree on which ones).

Approach

Type Dispatch

Data Payloads

Out-of-band Communication

Action at Point of Exception

Exceptions as Data

Notes

education on dynamic binding

solves

n/a

solves

could solve

n/a

 

clj-stacktrace

should unify with dataification of exceptions, if any

n/a

n/a

n/a

partially solves for non-Clojure types

 

ad hoc conditions

worsens

could solve

worsens

solves

orthogonal

worsens = replaces general mechanism with more specialized one

pattern-matching conditions

?

could solve

worsens

solves

orthogonal

worsens = replaces general mechanism with more specialized one

data-carrying exception

could support pattern matching

solves

n/a

n/a

solves for Clojure types

 

enrich exceptions with local context

n/a

enhanced

for errors only

n/a

enhanced

yuck: makes everybody pay for debug time support

bindable test/assertion handler

orthogonal

orthogonal

for assertions

for assertions

n/a

 

bindable throw handler

orthogonal

orthogonal

for Clojure exceptions

for Clojure exceptions

n/a

 

platform-based handler

orthogonal

orthogonal

solves

solves

n/a

does it exist?

bindable edge handler

orthogonal

orthogonal

tries and fails

tries and fails

could help

yuck: perf, asymmetry, doesn't accomplish desired goal

wrap java exceptions

orthogonal

orthogonal

at Clojure boundary

n/a

could support

yuck; terrible perf, terrible asymmetry, or both

"modest proposal" above

solves 

solves 

solves, with extra goodness for debug

solves, with extra goodness for debug

solves

what's not to like? see bottom of this page...

Education on Dynamic Binding

Document dynamic binding as the "Clojure Way" to do out-of-band-control flow that is often done via exceptions on other JVM languages.

  • find likely place within Clojure or Contrib and implement examplar
  • write docs and tutorial
  • add "control flow" link from exception handling parts of Clojure docs

clj-stacktrace

  • define a standard vocabulary for data-izing the information in a clojure exception
  • package in fns
    • do not call these fns automatically
    • maybe provide REPL helpers that do
  • other parts of clj-stactkrace (color printing etc.) should remain tool features
  • open questions
    • what do the REPL fns look like?
    • might start with the data and wait to see how they get used

Ad hoc conditions

E.g. Contrib Condition or Contrib Error-Kit.

  • could unify with Java exceptions, or not
  • could include data-carrying exception, or not

Not carrying this idea forward at this point.

Pattern Matching Conditions

Pattern matching (a la Matchure), plus exceptions as data, plus dynamic binding.

Not carrying this idea forward at this point.

Data-carrying exception

Single exception type that can carry data, allowing clojure data tools to be used to process exceptions.

  • subclass RuntimeException?
    • or one new type for each family (error, runtime, assertion?)
  • unify keys with clj-stackrace or equivalent if that is also done
  • open questions
    • what are the programmatic (non-interactive) uses of this data?

Enrich exceptions with local context

  • add locals, bindings, kitchen sink to exceptions
  • could be debug-only
  • could be tied to throw, or only to assertions

Bindable test/assertion handler

  • Create a bindable *assertion-handler* that can run with full context at the point an assertion fails
    • application of dynamic binding to the problem "Assertion fails where I can't get a debugger" (e.g. Hudson)
    • default behavior is to raise the error
    • facilitates use of assertion as the primary/only mechanism for tests, instead of separate fns like is
  • implicitly debug-only through tie to assertions
    • once assertions are debug-only, anyway
  • maybe assertions should never have been exceptions in the first place
    • more of a use case for handlers than for exceptions
    • unexpected errors are, ironically, expected errors during development
  • making assertions handler-based solves my "assertions are backwards" rant
    • handler can do anything it needs to with local context
  • also decouples the Java enablement issue
    • clojure binding controls initial reaction
    • Java -ea flag controls fallback behavior (assert or do nothing)
  • open questions
    • encourage test frameworks to bottom out at assert, instead of their own ad-hoc things?
    • what does the fn take?
      • source code forms of the assertion
      • want access to locals
  • very useful on CI
    • dump everything you know

Bindable throw handler

  • like the assertion handler, but tied to all (Clojure) throws
  • *throw-handler* should be available (in effect?) only under debug mode
    • pay for what you need
  • open questions
    • what does the fn take?
      • source code forms
      • access to locals
      • args to exception
  • very useful in CI
    • dump everything you know

Platform-based handler

Is there JVM support for automating handlers like the assert handler or throw handler? If these exist, and are flexible and easy to deploy, could use them instead of extending Clojure.

  • not aware of any
  • Clojure versions would work on other platforms

Bindable edge handler

  • try to make all exceptions handle-able by wrapping clojure/java boundary everywhere
  • when exception from Java land hits Clojure boundary, call the handler before propagating it
  • attempt here is to respond closer to the problem
    • but halfway between the problem and the outer handler isn't much better than a full stack unwind
    • nothing special about the Clojure/Java boundary
  • pervasive implementation and performance issues
  • non-idiomatic wrapping
  • ok, this is a crazy idea

Weaknesses of the modest proposal

  • still no automatic way to discover relevant bindings
    • documentation, of course
    • but no programmatic way to discover relevant situation handlers
    • checked exceptions got this half-right
  • Java/Clojure boundary visible on exceptions-as-data
    • Clojure exception can give you everything
    • with Java exception you have to ask by calling a fn
  • Java/Clojure boundary visible on throw handler
    • no straigtforward way to sneak into Java
  • if there is any commonality in how to write a bindable handler, it isn't captured here
  • nothing stops an exception from being unnecessarily data-ized multiple times
Labels:
  1. Jan 04, 2011

    This needs so much more about the problems it intends to solve. I want... doesn't count.

    At a glance,

    I wonder about deriving from Throwable - you don't want to encourage people to catch Throwable.

    Why isA map vs composition?

    If REPL works only in terms of maps, means every possible exception must be caught and wrapped/transformed - bleh.

    1. Jan 13, 2011

      • not deriving from throwable, plan is to provide a few different types in the families people need (e.g. errors, runtimes, assertions)
      • don't care much about isA vs. composition at this point
        • either could work with pattern matching
      • REPL could have raw APIs, but I think I will want cooked/wrapped ones as well
  2. Jan 15, 2011

    Thanks Stu!. You lose me at the grid. Anytime you have a grid dominated by orthogonal and n/a you have a problem. The problems and their possible solution should be considered separately. It will be a plus if a single solution solves multiple problems, but we need to decide which problems we want to tackle at all, and examine the strengths and weaknesses of solutions relative to each problem. We also need to pick apart some of those extant composite 'solutions', as they have many separate aspects. Having a grid that crosses spaces makes it seem like you have more alternatives per problem than you actually do.

    1. Jan 15, 2011

      I'll split them out in the next pass -- the grid is trying to do too many things at once. (Looking for a good way to (1) line up pros and cons of multiple approaches but also (2) not lose sight of interrelationships.) 

  3. Jan 15, 2011

    debug mode/debug-only needs to become a first class issue. The problems:

    • Some errors are expensive to detect, and we might want to presume don't exist in tested code so as not to incur the runtime overhead.
    • We have no sufficient way to control that right now

    If you are going to propose assertions for tests, you cross the line between ensuring correct use and ensuring correct implementation - these are different.

  4. Apr 26, 2011

    In order to help with debugging, it could be useful for tooling to replace the throw form, enabling inspection of the local environment at the point of the exception.

    throw is a special form currently, so this is not possible.

    Obviously only helps with exceptions thrown from clojure code.

    1. Apr 27, 2011

      some kind of compiler api that allowed 3rd party code to finesse the compilation of special forms like throw could also help with sandboxing

  5. Aug 16, 2012

    I just wanted to point out a particularly common case I'm running into in library code that I want to share between Clojure and ClojureScript: It is currently impossible to throw on the JVM without referring to a platform-specific type. You must do (throw (Exception. "omg!")) otherwise you get a ClassCastException and your error message is replaced by "java.lang.String cannot be cast to java.lang.Throwable"

    JavaScript allows you to throw strings, but you don't get the full context of a true Error object: (throw "omg!") vs (throw (Error. "omg!"))

    In my projects, I've been defining (defn raise [message] (throw ...)) per platform.

  6. Aug 16, 2012

    One other thought: Could functions be enclosed in an implicit try? It seems like both Clojure and ClojureScript gracefully ignore try forms without catch or finally subforms.

    Ruby functions have an implicit "begin" for exception handling. Consider the following irb session:

    irb(main):001:0> def f(x)
    irb(main):002:1> x * 2
    irb(main):003:1> ensure
    irb(main):004:1* p "ensure!"
    irb(main):005:1> end
    nil
    irb(main):006:0> f 10
    "ensure!"
    20