Go's proposed panic/recover exception-like mechanism

Here it is: Proposal for an exception-like mechanism by Rob Pike.

As I understand it, he means panic/recover primary as a way to shield the whole program execution from "truly exceptional situations" like out of bounds array access, etc. in "submodules". Like bug in request handling code taking the whole web server down.

What do you think about such hybrid "panic/recover (rarely) + error codes all over the place" error handling model?

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

I like Dylan's model

What do you think about such hybrid "panic/recover (rarely) + error codes all over the place" error handling model?

For me, Dylan pretty much nailed what I want from an exception system in a strict, object-oriented language, namely that exception handling should be separate from control flow. This is simply a nice and powerful model, that subsumes classic exception handling, and in addition allows much more programmability, robustness, and interactivity for error and signal handling.

Dylan's model has both

Dylan's model has both return-values from conditions and 'restarts' (a class of conditions used to continue a behavior). This strikes me as undesirable replication of behavior. I'm not a big fan of TIMTOWTDI language design.

That said, it beats the C++ or Java model for exceptions, which is non-resumptive.

Stories related to restart handling:

Restarts are just conditions

In Dylan, restarts are merely a stylized use of conditions.

In this example, you can see that restart handlers are just normal condition handlers, and invoking a restart is done via ordinary error ("throw").

Didn't I just say that?

Didn't I just say that? "'restarts' (a class of conditions used to continue a behavior)."

I simply find the idea of handlers exiting with meaningful return values to be quite funky. It isn't as though this is a normal situation, where useful modularity is to be had from processing return values from lower in the stack to higher in the stack.

Still puzzled

Dylan's model has both return-values from conditions and 'restarts' (a class of conditions used to continue a behavior). This strikes me as undesirable replication of behavior.

What replication do you mean?

TIMTOWTDI

There is more than one way to do it in Dylan: you can resume by means of return-value, or you may resume by means of restart.

Sorry, still don't get it

In Dylan, invoking a restart is conceptually nothing but calling a function one has found by walking up the thread-local handler stack, and looking for a matching handler.

That function has to obey the handler protocol (call the next handler, or handle the exception by returning something, or do a nonlocal transfer of control to abort).

I don't see how this introduces "more than one way to do it".

P.S. My point is that Dylan's condition system is merely a stylized use of functions, thread-local vars, and nonlocal escapes. So when using the condition system, there is in fact MTOWTDI (i.e. the three possibilities described above), but this doesn't make the language per se a "TIMTOWTDI language".

RE: still don't get it

It has always been my understanding that the very concept of 'function' is to capture a reusable block of code. This "stylized use of functions" requires special protocols. One must often lexically capture local exits into the stack, or call a special 'next_handler()' to do the job. When you reach certain levels of specialization it's reasonable to give that specialization a dedicated name. In this case, let's call it 'handler'. Now, if - as you propose - handlers are simply functions, then I would expect the ability to use and reuse handlers much like other functions. But what does 'next_handler()' mean if you call it from a regular function? Can one treat a function with exits to a specific stack frame the same as other functions? (You can in Scheme. Can you in Dylan?) To what degree may one one reuse handlers across contexts by abstracting non-local exits?

To mine eye, what Dylan did was introduce a standard exception throwing mechanism, then cludged the job by attempting to reuse the function concept for the handlers, thus forcing them to place rules upon programmers, rather than disciplining those rules via syntax.

If you're going to introduce a primitive or standard mechanism unto a language, you may as well introduce its whole set. Special rules here and gotchas there hurt uniformity and symmetry - and confidence - in the language. I object to use of locks on that basis. And to Perl. Dylan is far from the worst language in this respect.

You point to Dylan's "stylized use of functions" as excusing the multiple return types, and even as excusing the specialized handler protocol. ("What a clever reuse of functions!" the man believes.) I look at the same and see a specialized mess that would have been better served by providing a more complete 'handler' syntax to accompany the concept. ("What an inappropriate explicit reuse of functions!" the man believes.)

The MTOWTDI for the handler protocol is just a symptom. I do not mean to suggest that Dylan embraces TIMTOWTDI. If it did, I'd have decried Dylan as a whole, and not just the restart mechanism. ;-)

P.S. Dylan's restart mechanism and condition system is still 'superior' to the vast majority of its competition. But I believe that Dylan hasn't 'nailed' anything, and that it could do even better.

No funny stuff

Now, if - as you propose - handlers are simply functions, then I would expect the ability to use and reuse handlers much like other functions.

Of course. And all of your questions have simple and satisfactory answers:

But what does 'next_handler()' mean if you call it from a regular function?

Define the handler protocol so that a handler function receives a next_handler function as a parameter (in addition to the condition object). This is what Goo does.

Can one treat a function with exits to a specific stack frame the same as other functions? (You can in Scheme. Can you in Dylan?)

Of course, but of course you have to stay within the language's control flow regime: if you have multi-shot continuations, you're free to jump around as you wish, if not, handlers have to obey the usual rule that they can't jump to exits whose dynamic extent has ended.

To what degree may one one reuse handlers across contexts by abstracting non-local exits?

To the full degree the language allows for functions. If your language has first-class exits, handlers may use them as any ordinary function may.

But I believe that Dylan hasn't 'nailed' anything, and that it could do even better.

All in all, I think treating condition handlers as functions has worked well for the languages that have adopted this mechanism: Common Lisp, Dylan, Goo, and R6RS.

Regarding 'nailed', I just don't see anything that's missing or superfluous in Dylan's condition system. I'd be very interested to hear what you think Dylan could do to make condition handling (even) better?

all of your questions have

all of your questions have simple and satisfactory answers

So... where are they?

I just don't see anything that's missing or superfluous in Dylan's condition system.

And yet you already offered suggestions for improvement, such as providing 'next_handler' as a companion argument to the condition variable.

I do not see how you reconcile that behavior with this later assertion of yours.

next_handler

And yet you already offered suggestions for improvement, such as providing 'next_handler' as a companion argument to the condition variable.

That part is already in Dylan - I just wasn't sure when I wrote the previous post, so I cited Goo, where I was sure.

For the "next handler" question (as well as your other questions regarding closed-over exits in handler functions), I stand by my claim that there's a simple and satisfactory answer to it.

Your point that the language should provide specialized syntax for handlers is a non-issue for me, as I assume a language with syntactic extension. Furthermore, the Scheme philosophy of "if it can be defined with functions as arguments, we don't need a special form" has its merits, and IMO improves the programmer's "confidence" in language constructs, and enables reuse.

simple and satisfactory answers

I stand by my claim that there's a simple and satisfactory answer to it.

"Satisfactory" is not a property of an answer alone. I suspect you and I see different problems.

Your point that the language should provide specialized syntax for handlers is a non-issue for me, as I assume a language with syntactic extension.

I believe that 'protocols' imposed upon a programmer (aka boiler plate) should always be protected and supported by syntax. Since the protocol defined by 'Exceptions' is part of "The Dylan Reference Manual", the syntactic extensions to protect and support this protocol should already be part of the language.

I do not mind if this syntax is provided through a syntactic extension mechanism: indeed, I would the language's entire syntax be defined via the extension mechanism, and that syntax extension mechanisms should be non-monotonic (i.e. not restricted to 'extension').

Scheme philosophy of "if it can be defined with functions as arguments, we don't need a special form" has its merits

As I understand it, a great many (perhaps even most) uses of the syntactic extension mechanisms in Scheme are to simplify use of libraries that are, in fact, already defined with functions. The philosophy you describe would seem to contradict this. Where did you hear this philosophy?

and IMO improves the programmer's "confidence" in language constructs, and enables reuse

When you start dealing with 'protocols' - especially if they have any 'gotchas' for getting them wrong, and especially if they are used repeatedly and scattered throughout code - you need confidence in more than just the language constructs.

I don't think so

Since the protocol defined by 'Exceptions' is part of "The Dylan Reference Manual", the syntactic extensions to protect and support this protocol should already be part of the language.

Except there isn't anything to protect! Since handlers are ordinary functions, all the points you've raised (e.g. closed-over exits) are already handled, protected, and supported by the semantics of the core language: lambdas, lexical exits, unwind protection, and thread-local ("dynamic") variables, all of which are standard in many languages, are all that's needed from the core language to implement Dylan's condition system. (This is not merely a claim, I wrote a toy Lisp to find out.)

Scheme philosophy of "if it can be defined with functions as arguments, we don't need a special form" has its merits

As I understand it, a great many (perhaps even most) uses of the syntactic extension mechanisms in Scheme are to simplify use of libraries that are, in fact, already defined with functions. The philosophy you describe would seem to contradict this. Where did you hear this philosophy?

Yeah, the standard definition of some operator is defined with functions – in line with the philosophy I described – and then may or may not get wrapped in a macro that abstracts away the functions for convenience. (call/cc, dynamic-wind, define-syntax, are all obvious examples of this.) But since these transformations are so simple and common-sense they pose no danger to the programmer's confidence in the language: Compare Scheme's dynamic-wind (using functions) with Common Lisp's unwind-protect (functions abstracted away). By your argument, Common Lisp programmers should have less confidence in unwind-protect than Scheme programmers have in dynamic-wind. That's hardly the case.

(Err, upon rereading, there may be a big misunderstanding somewhere. YMMV.)

Note that some Scheme users even go so far as to write function definitions with aching verbosity in order to follow that philosphy:

(define my-fun (lambda () ...))
When you start dealing with 'protocols' - especially if they have any 'gotchas' for getting them wrong, and especially if they are used repeatedly and scattered throughout code - you need confidence in more than just the language constructs.

Re gotchas for getting them wrong: my point is that Dylan's condition system has few if any gotchas unrelated to the intrinsic, irreducable complexity of dealing with restartable exceptions. Especially because Dylan's system is built on standard facilities like lambdas, lexical exits, unwind protection, and dynamic variables, which is what I'm arguing for.

Re confidence: Yes, in the scenario you describe you need confidence in the (other) programmers, but that is not a PL issue (for the purposes of this discussion ;)).

P.S.

syntax extension mechanisms should be non-monotonic (i.e. not restricted to 'extension').

Please explain!

Except there isn't anything

Except there isn't anything to protect!

Nothing to protect, eh? Well, I'll agree that attempting to fix the condition system would be a little silly. What needs protecting is much bigger than that: Dylan is NOT a safe language with respect to the 'function' abstraction. Programmers cannot be confident that function values obtained as return values, nor as parameters in a concurrent scenario, will be safe and well-defined. The entire 'function' abstraction is full of gotchas. And, therefore, the entire function-passing protocol should be protected.

(Err, upon rereading, there may be a big misunderstanding somewhere. YMMV.)

Yes, I suspect so.

Re gotchas for getting them wrong: my point is that Dylan's condition system has few if any gotchas unrelated to the intrinsic, irreducable complexity of dealing with restartable exceptions.

Err, it inherits many, many gotchas orthogonal to the intrinsic, irreducible complexity of dealing with recoverable errors. As one example, Dylan's condition system doesn't even start to consider how error recovery policy is to be packaged up into lazy or parallel computes, which forces the developer to bypass / reinvent the condition system entirely to support error-recovery in those cases.

syntax extension mechanisms should be non-monotonic (i.e. not restricted to 'extension').

Please explain!

I am asserting that the syntax-extension mechanism should be capable of modifying the syntax, or swapping it out entirely, with no vestigial remnants from the 'initial' syntax - i.e. you aren't stuck with keywords, parentheses, the format for line or region comments, a particular class of numeric tokens, etc. Ideally no reserved UTF-8 characters... you can even make whitespace significant, if you wish. Inherently, this means the syntax-extension syntax needs to be defined entirely within the syntax-extension mechanism, lest you would be unable to remove it as 'vestigial'. Even the ability to modify the syntax can be modified. These modifications should be 'spatial' in nature, such that you can specify the syntax for regions of code (though 'the rest of the file' is a valid region).

This greater level of flexibility allows one to restrict or specialize syntax for certain regions of code. It also allows one to more readily develop or experiment with new languages, without needing to escape the primary IDEs. Data definition and is an especially big profit area, ranging from regular-expressions and math to interactive fiction developed in Japanese or German. Usefully, one could integrate graphical and text editors, with graphical editors producing text they can both read and write, specialized for the change patterns they expect.

John Shutt has done a lot of research on this subject, and the whole idea was quite big many years ago. There have been a few attempts to revive it, i.e. as 'Language Oriented Programming'.

The approach I've been pursuing is based on Knuth's attribute grammars. I tweak that concept just a little: the entire grammar becomes one of the attributes. This gives me most of the properties I want, except for type-based overloading of operators, and the 'appendix' style {EXPRESSION} WHERE {DEFINITIONS} syntax. It also lets me capture the syntax definition at a particular point in code (including the lexical scope if desired), and export it or use it to drive a runtime parser.

Structured syntax extension?

I am asserting that the syntax-extension mechanism should be capable of modifying the syntax, or swapping it out entirely, with no vestigial remnants from the 'initial' syntax - i.e. you aren't stuck with keywords, parentheses, the format for line or region comments, a particular class of numeric tokens, etc. Ideally no reserved UTF-8 characters... you can even make whitespace significant, if you wish.

Do you see such a facility as possibly occurring within a programming language? Off hand, it seems to me that one couldn't use the term "programming language" for anything that contained such a thing; the facility would have to come under the heading of "compiler technology" or some such. (Like YACC.)

Regarding syntax extension as a programming-language facility, I agree syntax extension should be non-monotonic. The historical Extensible Programming Languages movement failed in significant part because it lacked information hiding: a new layer of extension could never get away with ignoring details that had been extended away from in the past (i.e., details could be "extended" away from, but not "abstracted" away from). And non-monotonic syntax extension seems to me to be a sort of information hiding.

There's another problem, though, with the historical notion of syntax extension: because it was essentially unstructured, when you looked at a block of code depending on a bunch of extensions, you might not even know how to parse that code. Granted that we're accustomed to not knowing what some of the symbolic names in the code mean, until we go look them up (which is generally pretty straightforward to do); not knowing which sequences and subsequences of tokens make up subexpressions seems to me to be a much more serious problem. (I'll generously assume that one can reliably tell where the tokens begin and end.) That's why, after my first efforts at extensible syntax (way back in the mid-1980s), I started to look for some basic notion of well-behavedness for extensible syntax parsing behavior. I finally produced a techreport in 2008 — which tries, at least, to be somewhat independent of what particular syntax formalism is used, such as the variant of AGs you describe.

Do you see a place for structure in syntax extension?

Re: structured syntax extension

one couldn't use the term "programming language" for anything that contained such a thing; the facility would have to come under the heading of "compiler technology" or some such

It is unclear to me what distinction you are attempting to make. Assume a live programming environment, with JIT compilation. Where does the "language" stop and the "compiler technology" start? or vice versa?

It is important to me that the IDE be aware of this technology, and that documentation on this technology be readily available to users, and that communities be able to develop and standardize syntax to model and manipulate the domains relevant to them (no matter how obscure), and even support nationalization of the language. Somewhere below the syntax is the AST, which should have a tight correspondence with the language's semantic definition.

The historical Extensible Programming Languages movement failed in significant part because it lacked information hiding [... and] because it was essentially unstructured

Interesting. I've never looked into why it failed. I always assumed it a case of 'worse-is-better'.

unstructured: when you looked at a block of code depending on a bunch of extensions, you might not even know how to parse that code [...]

Do you see a place for structure in syntax extension?

The way extensible attribute grammars work is that, when you reach any block of code, you have a complete formalization of both the grammar and the goal. While parsing that block, there may be rules within it that change the grammar for further parsing. To support descriptions of syntax as a library feature, a special mechanism is provided to import grammar from external resources (for my language, from another page of code).

I consider it important that the import mechanism be well-defined, and that upon entry to a new 'page' of code that the initial grammar be well defined, and that the grammar be well-defined at every point during the parse. For my own language, I am quite tempted to assert that the only legal statement upon entry to a new 'page' of code is the import statement for the initial grammar for the rest of the page. This would support versioning and deprecation of syntax over the language's lifetime. I much favor spatial extensions, and provide no support for temporal ones (i.e. everything is using pure functions during the parse, except for imports which are treated special).

But, certainly, there are blocks of code within pages that you would not be able to parse without more context... i.e. without knowing the initial grammar that applies to that block, or without also looking up the external resources that define imported syntax.

I made a start on reading the tech report when you gave me the ref a while ago, but I am not sure how my priorities fit into your taxonomies. You decide for yourself whether I see a place for 'structure' as you define it.

Continuing the tangent

I developed an extensible syntax for my language that supports almost arbitrary sub-langauges. I say 'almost' because I wanted the property that the outer language knows where the inner block should stop without actually running the custom parser. Letting sublanguages rewrite the entire grammar is the easy part. The harder part of course is brokering the peace between different smaller syntax extensions.

The harder part of course is

The harder part of course is brokering the peace between different smaller syntax extensions.

Yeah, that's roughly what I was after in the techreport. I wanted the parser behavior to be "cumulative" (admissibility of a parse tree depends only on whether its syntax rules are in the grammar), and at the same time "unambiguous" (at most one parse tree per input). Well, those and "inductive" (a parse tree is admissible only if all its subtrees are admissible). I ended up with a class of grammars that's formally undecidable.   [edit: sp]

Guaranteed termination was

Guaranteed termination was an important property for me, as was the ability to produce useful error messages for the users. And, of course, that "cumulative" property mentioned by John... I'm not certain I'd actually call something a 'parser' if it didn't have that rule [edit: unless 'admissibility' also means passing the other filters... such as typing.], but John's probably seen more crazy ideas than dreamed of in my philosophy.

I do not consider the potential for ambiguity to be a bad thing. Ambiguity is only bad when it actualizes. Indeed, I consider the potential for ambiguity to be a very acceptable trade if it allows developers to more easily express extensions or reduce verbosity. (Who cares whether "green ball" might be ambiguous in some context, so long as it isn't ambiguous in the current context?) Therefore, I choose to not require 'at most one parse tree per input'. I would expect to raise an error or warning if more than one arises, though. And it is important to produce a good error message for that case - framed in terms of the original syntax, not the underlying AST.

I've also explored the possibility of an ambiguous grammar becoming disambiguated later via the type-system filters.

admissibility; ambiguity resolution

I did mean admissible to include filters like type etc. A "parser" (generic term for any function from grammar, start-symbol, and string to set of parse trees) is cumulative when it has a set of parse trees that it allows, and it always returns all the trees it allows that are supported by its inputs.

Re potential for ambiguity — I defined the well-behavedness properties for a grammar/parser combination, so a parser is ambiguous on this grammar iff there is any choice of start-symbol and string for which it would return multiple trees. The idea being that I'd like to be able to warn about potential ambiguity when the grammar is created, rather than have some poor hapless developer be told that his code is ambiguous because somebody else in the past botched the grammar. (Like an error entry in an LR parser table whose message reads "error in language design".)

Re ambiguity resolution — that's actually in my treatment too, after a fashion. I suppose some equivalence relation E on parse trees for each string (e.g., parse trees are equivalent iff they differ only by associative grouping), and I want the parser to be "well-behaved E-complete", meaning that if the parser is well-behaved on grammar g, then for every parse tree t freely generated by g, the parser allows some tree E-equivalent to t. Here's what I skipped when I said I ended up with an undecidable class of grammars: For any E, let G be the set of all grammars for which there can exist a well-behaved E-complete parser. Does there exist a parser that is well-behaved E-complete on all of G? For some choices of E there is such a universal parser, for other choices there isn't; but there is a maximal choice of E for which there is one. And it even has a universal parser that halts on all inputs. However, given an arbitrary grammar, it is formally undecidable whether or not the universal parser is well-behaved E-complete on that grammar.

So if you insist on the largest possible choice of E, you can't always detect problematic grammars before you're actually presented with a source string that creates the problem. (And if you don't choose the largest possible E, somebody else will surely want a different choice. :-)

[copyedit - "because"]

It is unclear to me what

It is unclear to me what distinction you are attempting to make.

I just couldn't imagine stretching the word "language" to cover something that has substantially no fixed grammatical characteristics. It was the concept I was really after, and you responded most effectively by further explaining the concept; I'm comfortable calling it an IDE.

I'm having a bit of trouble working out the scale of what you have in mind. Say I've got a couple of hundred source modules, averaging about fifty lines of source code each (including comments). Would I expect them all to have the same grammar, or would each have its own customized grammar importing extensions from all the other modules it depends on? (And in the latter case, if I change one of the other modules it depends on, does this module's AST get recomputed from its source code based on the new grammar, or does its source code get recomputed from its AST based on the new grammar?)

Scale of Language Extension

Say I've got a couple of hundred source modules, averaging about fifty lines of source code each (including comments). Would I expect them all to have the same grammar, or would each have its own customized grammar importing extensions from all the other modules it depends on?

Well, in the technical sense, each would have its own grammar, if only because the names for variables visible in a specific scope would be the purview of the syntax extension mechanism.

But if you were to organize a few hundred source pages, most of them would be specializations of just a handful of grammars. These ten modules over here describes some relevant data for CGI animations and so imports from a syntax designed by experts and useful for that purpose, the dozen over there describes a dataflow graphs and so imports a syntax designed to interact with a visual dataflow-graph tool, another ten over here are specifications for the bot AI framework, these three describe the system's heads up display... And so on.

Importing a resource from another page would not automatically import the syntax from that page. And most pages would not export a syntax. (Import is not C/C++ style 'include'.)

In the context of 'large' projects, it isn't unusual for programmers to work directly with a half-dozen languages or more (SQL, Makefiles, a variety of XML languages, HTML, JavaScript, Lua, comment documentation, preprocessors, rules and heuristics, an imperative language, regular expressions, etc.) plus a few indirectly with support of tools (3D models, WYSIWYG, project management, ...). When you start counting the various "program languages" atop these substrates - especially common for frameworks and anything requiring boiler-plate protocols - the number of 'languages' balloons ever further.

I am suggesting that all of these be placed upon a common semantic substrate - which, in a sense, is a language all its own. That language, of course, must be suitable for end-to-end systems development (and my horse wants me to add: "that includes concerns for code-distribution, integration, testing, and upgrade".) One note is that this semantic substrate must effectively handle the tasks that are normally handled via explicit compilation... i.e. via staging, or compile-time resolutions, or partial-evaluation and lazy specializations. My recent focus on 'live programming' has me leaning more towards the latter: explicit staging involves very funky semantics regarding potential side-effects while modifying code.

if I change one of the other modules it depends on, does this module's AST get recomputed from its source code based on the new grammar, or does its source code get recomputed from its AST based on the new grammar?

Definitely the former: the AST is recomputed based on the new grammar, same as if you updated a function or a framework.

Aiming for the latter would require a different class of technology entirely.

OK

dmbarbour: Well, I'll agree that attempting to fix the condition system would be a little silly. What needs protecting is much bigger than that: Dylan is NOT a safe language with respect to the 'function' abstraction. ... And, therefore, the entire function-passing protocol should be protected.

We are in agreement that Dylan, inside its paradigm, has a nice solution for condition handling. Whether the whole paradigm is flawed is a totally different question. ;)

IME, Dylan and similar languages like Goo, EuLisp, and ISLisp are simply fine to program in. For programming in their "dynamic, object-oriented" paradigm, for lack of better word, where these languages still are avant-garde.

Programmers cannot be confident that function values obtained as return values, nor as parameters in a concurrent scenario, will be safe and well-defined.

Whether Dylan's "function abstraction" - and I assume that you mean in particular the danger of closed-over lexical exits, whose dynamic extent has ended - is flawed I can't say.

The DRM says "it is an error" if such a lexical exit is jumped to. This probably means it is up to the implementation whether such behavior should be detected or really, as you say, may lead to unsafe and undefined behavior. (Which would be bad, of course.)

However, staying on this particular detail, actually detecting whether the dynamic extent of an exit has ended is done easily. So, in the face of such "dangling" closed-over exits, a language could still be safe and well-defined by replacing the above phrase in its specification with "an error is signaled". If you count runtime errors under "safe and well-defined", which I do in general, for the kind of programming done in these languages.

Apart from that detail, I can't see any impediments to the safety and well-definedness of Dylan's function abstraction.

As one example, Dylan's condition system doesn't even start to consider how error recovery policy is to be packaged up into lazy or parallel computes, which forces the developer to bypass / reinvent the condition system entirely to support error-recovery in those cases.

As you are surely aware, the DRM has nothing to say about concurrency or lazyness. These are "outside the paradigm" of Dylan, as defined in the DRM. (That's not necessarily a bad thing, as implementations usually come with threads for example.) Judging Dylan's condition system on the basis of features which are (I assume) deliberately left-out of the specification of Dylan doesn't seem fair.

Judging Dylan's condition system

Judging Dylan's condition system on the basis of features which are (I assume) deliberately left-out of the specification of Dylan doesn't seem fair.

The position you are implicitly taking - that you should not judge languages and their elements by features deliberately left out of their specification - would require you give Java's condition system equally high marks (they deliberately left out resumable exceptions, among other things such as extensible syntax, etc.). Heck, it would require you give InterCal's error handling equally high marks, which is absurd. Reductio ad absurdum.

Languages - and their specifications - must be judged in terms of suitability for a purpose. Inherently, any language that purports to be general purpose must be judged for a wide range of purposes.

DRM says just as much as the C and C++ standards about concurrency. All three languages should be judged quite harshly in that respect (historical context notwithstanding; languages must evolve too).

The DRM says "it is an error" if such a lexical exit is jumped to. [...] actually detecting whether the dynamic extent of an exit has ended is done easily. So, in the face of such "dangling" closed-over exits, a language could still be safe and well-defined by replacing the above phrase in its specification with "an error is signaled".

Dylan's own decisions about its stack discipline were an intentional choice to sacrifice safety to gain a smudge of performance. One cannot 'easily' detect when a dynamic extent of an exit has ended without first sacrificing their reason for having made the choice in the first place - i.e. even ignoring the concurrent case, they would need to keep information in the heap that will survive further reuse of a stack region.

Apart from that detail, I can't see any impediments to the safety and well-definedness of Dylan's function abstraction.

Safety and well-definedness issues are almost always found in the interactions between features (including those that later get hacked into the language because of inadequate specification). If you don't see any while looking at a specific abstraction, that may be because you're searching the wrong space.

Puzzled again

DRM says just as much as the C and C++ standards about concurrency. All three languages should be judged quite harshly in that respect (historical context notwithstanding; languages must evolve too).

Saying nothing about concurrency can't be all bad. C offers probably the largest pool of concurrency mechanisms of any language.

Dylan's own decisions about its stack discipline were an intentional choice to sacrifice safety to gain a smudge of performance. One cannot 'easily' detect when a dynamic extent of an exit has ended without first sacrificing their reason for having made the choice in the first place - i.e. even ignoring the concurrent case, they would need to keep information in the heap that will survive further reuse of a stack region.

So are you saying that because of that small redundancy (having to store a jump buffer, or at least some metadata about it, on the heap), the whole stack discipline should be abandoned? If so, what stack discipline are you proposing as a replacement?

Saying nothing about

Saying nothing about concurrency can't be all bad. C offers probably the largest pool of concurrency mechanisms of any language.

Ah, the wonderful thing about standards is that there are so many to choose from, eh?

Saying nothing is, itself, a decision with consequences. The consequences here affect portability, safety, compatibility between modules, and scalability... all in negative ways. To do worse, you'd have to make an effort. :-)

Choice, by itself, is a bad thing. Ideally we'd have one big hammer and everything would fall before it efficiently, safely, and so on. Choice is at the root of all complexity, and is quite expensive. Sometimes it is necessary to accept choice to obtain some other feature, but it should never be considered an end of its own.

You should take C/C++ as an example that, if you don't choose how to handle a critical feature like concurrency, it will be chosen for you, in a dozen different potentially incompatible and unportable ways, and much less likely to cooperate with the other language features. At least, that's what happens for a 'successful' language.

So are you saying that because of that small redundancy (having to store a jump buffer, or at least some metadata about it, on the heap), the whole stack discipline should be abandoned?

No. You could keep it. You just wouldn't be gaining as many performance advantages from it. Small heap-allocations per frame really don't cost much less than do larger ones.

If so, what stack discipline are you proposing as a replacement?

Why assume a stack discipline is needed at all? If I were to propose a replacement, it would be to place activation records in a nursery heap, keep it clean with copying GC so you can use the 'increment a pointer' allocation, and compile to full continuation passing style. I mentioned this in an earlier post in this topic.

Signals and Non-local transfer

The fundamental mechanism here is that of Signal Handling, and Non-Local Transfer.

Restarts are just an idiom build upon these two mechanisms.

I'm a fan of exceptions,

I'm a fan of exceptions, except for situations like parsing, where you want to accumulate errors rather than abort whatever you're doing. I despise the model where you have to manually return error codes everywhere, and check the error codes whenever calling everywhere. Almost the only thing you can ever do in response to an exception or error is pass the buck back up to your caller. (Actually, this is the litmus test that indicates whether you should be using APIs that throw exceptions or not.) Error codes turn into a poor implementation of exceptions, where it's easy to forget to check for a return value and accidentally break the chain between the point of an error and the code that handles it.

The code that handles exceptions should normally be in the topmost loop of the program - that request handler loop should be wrapped in an exception handler, so an exception in the request handler should never take down the program. It's primarily because it's so incredibly rare that you want to do something in response to an exception that I'm so opposed to error codes.

Having stated my case so vociferously, now I want to point out where I do support error codes - or rather, success codes. If the alleged error is reasonably expected to occur, such as parsing an integer out of a user provided string, then one should probably use APIs that don't throw exceptions for these particular failures. Exceptions shouldn't be used for operations that are reasonably expected to fail occasionally, unless the operation is transactional in nature, and the process of unwinding that transaction that falls out of an exception handling framework is the desired response.

So for example a document editing program trying to re-open a file from a most recently used (MRU) list, and failing because e.g. a network location is no longer available, might reasonably use an exception to propagate the nature of the failure back to the event dispatching loop. The dispatching loop should then be able to display the exception's message to the user in a dialog box. This makes it important that the message is localized and is of high quality at the point of the exception throw, or else that the exception is wrapped by a handler at some point on its journey back. Exceptions can also have things like help identifiers associated with them for extra help in that dialog box.

What one really wants to avoid, and what error codes encourage, is loss of specificity as to the nature of an error on its journey between the error point and the point where the user is informed. If you have error codes defined out of a limited set of predefined constants at any point in the abstraction stack between the error and the user, there's likely going to be some coercion and the user will be stuck with a generic error message that they have no hope of diagnosing and fixing, without groveling around for logs buried somewhere in the file system.

PS: FWIW, folks decrying exceptions based on experience with C++ or Java are missing the point, IMHO - neither of those two languages has a good implementation exceptions.

Returning "Codes" such as error codes or success codes

I basically disagree with everything you just said. ;-)

I'm a fan of exceptions, except for situations like parsing, where you want to accumulate errors rather than abort whatever you're doing.

Except if you need to accumulate errors, than that is a design issue unique at the time you are writing the parser. No such need for exceptions here. I also think accumulate/abort is orthogonal decision to the specification of parsing.

The code that handles exceptions should normally be in the topmost loop of the program - that request handler loop should be wrapped in an exception handler, so an exception in the request handler should never take down the program. It's primarily because it's so incredibly rare that you want to do something in response to an exception that I'm so opposed to error codes.

This is completely wishy-washy, and carries no semantic advice whatsoever on how to write programs. Ergo, you cannot use what you just said to justify a feature in a programming language.

WHO CARES if it's incredibly rare that YOU want to do something in response to an exception? That's fuzzy thinking. You might be "right", but not with such fuzzy thinking.

The request handler loop is the super system. It delegates to subsystems. If there is an exception in a subsystem, then it is the responsibility of the supersystem to handle it. This is pretty much how Erlang works, and is very much The Right Way To Do It. Moreover, if a supersystem does not know about all possible failure modes for a subsystem, or has deliberately not handled a known failure mode, then the supersystem can have a catastrophic failure, due to violation of Run-To-Completion semantics. That's it. THAT's the killer argument. RTC semantics. Do you have it in your system? 'NO'? You lose, because any decision you make about software quality while the answer to that is 'NO' is utterly meaningless.

Exceptions shouldn't be used for operations that are reasonably expected to fail occasionally, unless the operation is transactional in nature, and the process of unwinding that transaction that falls out of an exception handling framework is the desired response.

More wishy-washy. There is NO reason to use exceptions for RTC-Semantics, which is exactly what transactional interfaces require.

What one really wants to avoid, and what error codes encourage, is loss of specificity as to the nature of an error on its journey between the error point and the point where the user is informed.

No. What one wants to avoid is system design that allows errors to propagate and hard to trace back to sources. The idea that dumping the stack is helpful is mostly a backwards idea and a curious accident. What most people like about Exceptions in Java is you cannot coerce the stack dump, e.g. no "loss of specificity". But if you need the stack dump, then chances are you only need it to pin down a really poorly designed abstraction, anyway, such as scattering object instantiation logic across client classes, rather than centralizing it and performing validation all in one place.

So for example a document editing program trying to re-open a file from a most recently used (MRU) list, and failing because e.g. a network location is no longer available, might reasonably use an exception to propagate the nature of the failure back to the event dispatching loop.

I would never write it that way. If the FileOpener object throws an exception, I would swallow it and send a signal instead - and only to the appropriate subsystem. The idea of an exception propagating back to a dispatcher makes no sense to me, since a dispatcher is fire and forget. Dispatcher literally should not care, other than to hear from the worker "Done!" (and only if the application requires reliable messaging). Bottom line: I'm not going to let some file system API designer or file dialog widget designer dictate to me how to express my subsystem/supersystem relationships.

Bottom line: I'm not going

Bottom line: I'm not going to let some file system API designer or file dialog widget designer dictate to me how to express my subsystem/supersystem relationships.

This, I think, gets at the heart of what I disagree with. The abstraction layer between supersystems and subsystems breaks down when errors occur; you want a layering violation in order to truthfully report the actual underlying error. An error defined in terms of the abstraction between subsystems is no good; the error may have come from a yet deeper subsystem, and hearing about it through a chain of Chinese whispers just makes it next to impossible for the human - the ultimate supersystem - to diagnose the actual problem.

The dispatcher is the agent of the user. When something is dispatched, and it failed for some reason, the user wants to know. And that's why it's the responsibility of the dispatcher to collect that reason and show it to the user.

All that's not to say that e.g. exceptions can't be wrapped in higher-level semantic reasons, e.g. consider a chain that said couldn't open the file because -> file system is corrupt because -> mechanical failure in the hard disk. Each of these errors come from different subsystems, but for the truthful nature of the problem to propagate out of the system, data from the level of the error occurring needs to get all the way back to the user.

Coupling control flow and data flow in response to errors

This gave me an idea. You advocate the importance of making sure error data makes it in tact to the user or a trusted agent. This amounts to a data flow requirement. Others express concerns about particular one-size-fits-all control flow strategies in response to errors. Can we simultaneously satisfy both of these data flow and control flow requirements?

One idea I had is to give error description objects linear types. If we protect the ability to discard an error description object via some sort of effect system (e.g., put discardError in the IO monad), then unprivileged code has no choice other than to eventually deliver the error to privileged code. But it does have quite a bit of freedom in terms of what else it might do first.

I haven't fully thought this through; I'm sure there are a few issues with making it really work in practice. For example, what if code that should return an error simply doesn't terminate? Nonetheless, I think there is some attainable middleground.

Trusted agent

Sameer,

In what sense do you speak of a trusted agent?

The way I see it, going off Barry Kelly's example, if a network resource is inaccessible, we may want to delegate to the trusted agent to resolve the problem. For example, in a distributed system, being disconnected is virtually indistinguishable from weak or episodic connectivity. Thus, when the trusted agent is sent a message concerning the connectivity problems to some stream of data, it has to pick the best strategy for resolving this failure. In this sense, the trusted agent acts as the user's attorney. Therefore, it is a data flow requirement, because anything the trusted agent would do, would ideally be anything the user could interactively do (i.e., click Refresh over and over again) but potentially more intelligently (receive a message from the operating system notifying it that connectivity is back up). A separate issue is how to handle resumption of whatever stalled control flow there was.

One consequence here is that "eventually deliver the error to privileged code" is not clear enough semantically (IMHO) and potentially leaving the requirement at just that damages the cohesiveness of both the trusted agent and the subsystem. You would also need the ability to coalesce successes or failures (e.g., for weak or episodic connectivity issues), because the agent is ultimately the one that determines resumption of stream processing. And perhaps the best alternative is to re-structure things so that the trusted agent does all the network activity, and the subsystem simply triggers the trusted agent whenever it demands data from the network stream. The subsystem then does not care about the implementation of the trusted agent, because all it does is trigger the trusted agent. The trusted agent in turn simply knows to pass by-value the data packets the subsystem requested. If the subsystem needs to be alerted of pipeline stalls e.g. due to connectivity, then that should be modeled using a separate static relationship between the agent and the subsystem. In this way, the subsystem can handle partial failures internally without tightly coupling to the agent.

Contrast, w/ Rob Pike's view

Rob has apparently tried to clarify his thought process in the aforementioned thread in the OP. He explains why panic/recover is the way it is.

I don't think his reasoning is sound at all. Giles Lean, if anything, is the voice of sanity in that thread. He notes that traditional structured exception handling allows spooky action at a distance, making reasoning about program behavior more difficult. My rebuttal to that is that you should not couple exception handling to run-to-completion semantic requirements; any spooky action at a distance is literally violating some RTC semantics your program requirements are supposed to have but you as a programmer are deliberately ignoring coding.

Rob appears to be objecting to programmer's who write code like that, and I am saying if you write code like that you don't know what you are doing and are using structured exception handling as a crutch. I don't think you can socially engineer a solution to this problem like Rob seems to be suggesting. Actually, you only make it worse, re-using Andreas Rossberg's argument above that this is richer and less structured. Just look at the rest of the thread where people are asking classical questions such as divide by zero exceptions, asking for stack traces, etc. It shows you how there is no place to draw the line socially.

This is actually one of the hallmarks of Pi calculus, petri nets, and the actors model: partial failure of a distributed computation is obvious and what to do in the presence of such failures is obvious. There is no social engineering in those models. Most importantly, in the Pi calculus, you are distancing yourself from the von Neumann model and notions such as scoping levels really don't come into play. Instead, failure is as fine-grained as communication of failure between processes.

re: Pike's view

You got me interested, Z-bo, so I spent some time going over the details and catching up on Go and this feature. Rather than waste that, even though I don't have an especially fancy, theoretical take on it, I'll say:

I think the design they put forward makes a mistake, based on their own stated criteria, the heart of which I find here (from Pike):

Panic and recover are not an exception mechanism as usually defined because the usual approach, which ties exceptions to a control structure, encourages fine-grained exception handling that makes code unreadable in practice. There really is a difference between an error and what we call a panic, and we want that difference to matter. Consider Java, in which opening a file can throw an exception. In my experience few things are less exceptional than failing to open a file, and requiring me to write inside-out code to handle such a quotidian operation feels like a Procrustean imposition.

Our proposal instead ties the handling to a function - a dying function - and thereby, deliberately, makes it harder to use. We want you think of panics as, well, panics! They are rare events that very few functions should ever need to think about. If you want to protect your code, one or two recover calls should do it for the whole program. If you're already worrying about discriminating different kinds of panics, you've lost sight of the ball.

In that case, I do not think that panic should be able to pass a value which becomes the return value of recover. Indeed, it's very awkward when they try to do so by having the declared type of that value be interface {} indicating a value you can't do much with without interrogating its type using the reflection facilities.

A simpler proposal might have been to replace panic with a parameterless abort and recover with a parameterless aborted that returns 0 or 1. Then we have a simple rule that every function whose body you can't inspect in detail may very well abort. It's fairly obvious that the feature isn't intended to be used for any "fine grained exception handling". And it's pretty hard to abuse the feature against its intended uses because a non-trivial use of aborted will stand out like a sore thumb under even light code review.

They seem to have wanted little more than a handy way to register and de-register dynamic "clean-up handlers", normally for events that end with program termination but perhaps in some server contexts. They wanted something like Posix exit and atexit but adapted to context. Why they threw in a parameter to panic is a mystery and probably a mistake.

Maybe a better name than abort and aborted, even, would be something like give_up and gave_up. In a command line context, the code that happens if gave_up is true might do something no more complicated than ensure an exit status of 2 (on a unix system). In an HTTP server, spawning little go routines to handle incoming requests, if the central loop hears that request handler gave up, it can at least tear down any central loop resources for that handler and send a 50x-whatever generic response. That seems to be about all they want it for and in that context, the parameter to panic and return value of recover seem like a kind of too-much-coffee thing.

My beef with exceptions....

Exceptions are used too often to "catch" programmer errors, bad array indexing being a canonical case. I believe programmer errors should either (1) abort the program or (2) just yield undefined program behavior.

What?

Exceptions are used too often to "catch" programmer errors, bad array indexing being a canonical case.

This is a good thing. I don't know about you, but I don't output perfect code 100% of the time.

I believe programmer errors should either (1) abort the program

I believe I understand the logic here, die early and die loud, correct? I agree in principal, but some errors can be gracefully handled. Why stop that?

or (2) just yield undefined program behavior.

Seriously? Is it April's fools day in your timezone? I can't imagine a singe situation where I would ever want undefined behavior. C allowed undefined behavior all over the place. It made it unpredictable how code would act between compiler versions, never mind the amount of effort that was required to port to windows msvc instead gcc.

It sounds to me like what you really want is more powerful static analysis. My understanding of dependent typing is limited, but I think it might be a step in the direction you seem to be headed. Maybe someone with more knowledge can jump in.

This is a good thing. I

This is a good thing. I don't know about you, but I don't output perfect code 100% of the time.

The idea Scott seems to be communicating (to me, anyway), is that the client programmer is violating the API author's contract.

Come to that, there are certain kinds of "Exceptions" that mostly exist due to imperfect systems, rather than merely imperfect code.

Going off an example from another recent thread that mentioned Liskov Substitution Principle, Eiffel's type system has "holes" in it that prevent truly modular extensibility. Instead, Eiffel, IIRC, does global data flow analysis to guarantee covariance is preserved in the face of such extensions. Yet even this does not catch all problems, and some issues are not detectable until runtime. If a system such as this were to raise an exception due to invalid coercion (more likely it would crash, but that depends on the environment and language specification), then that's just another example of "catching programmer errors". -- Bad array indexing is really just the lack of refinement typing.

I believe I understand the logic here, die early and die loud, correct? I agree in principal, but some errors can be gracefully handled. Why stop that?

Actually, you're not too far off. The issue with graceful error handling mostly has to due with nesting environments in a way that allows automatic sandboxing of untrusted code. From this perspective, any time the client programmer calls some third-party API without making sure he/she obeys the contract, then an automatic sandbox should be set-up, segmenting the address space so that the third-party does not bring down the enclosing environment.

Edit: Dependent typing, by the way, solves nothing here (at least not directly). The issue is calling into any API based on the von Neumann model or control flow model of computing, with (1) a program counter that steps on addressable instructions (2) global updateable store. Therefore, effect typing and static analysis of untrusted code might be more realistic solution.

I find it very difficult in

I find it very difficult in the format of these threads on LtU to see exactly what post is being responded to, so maybe I missed some context.

The idea Scott seems to be communicating (to me, anyway), is that the client programmer is violating the API author's contract.

Nothing in Scott's post indicated to me that he was referring specifically to working with a 3rd party API or even a separate module written by the same developer. If this is what he meant, then we are in agreement.

Unfortunately, I work with a particularly poor API during my day job at times (no names to protect the guilty). It did not take very long for me to come to the conclusion to simply let exceptions from said API propagate to the top level, where I then log them and die.

Looks like a good model to me.

I'm a fan of the kind of hybrid approach Pike is talking about. Have calls that run into errors return error values, and any procedure that gets an error value returned to it from any call, unless it is specifically a handler that can cope with that error, just returns that error value, automatically propagating it back down the stack of callers.

A smart compiler ((handwave inserted here)) can do this via longjmp/setjmp directly to the handler, but the "simple" semantics are easier to code around.

Have calls that run into

Have calls that run into errors return error values

I like that, but because Go lacks unions, when it returns an error code, it still also returns the "success" value. So you end up with this zero-inited zombie value that you must not use, but it doesn't prevent you from using it.

any procedure that gets an error value returned to it from any call, unless it is specifically a handler that can cope with that error, just returns that error value, automatically propagating it back down the stack of callers.

That would be nice (and sounds more or less like exceptions to me), but Go doesn't do that as far as I know. You have to manually check for the error and then return it. Some example Go code I've seen has a lot of that boilerplate.

I'm not convinced

It is ironic that their motivation is to tame and restrict the use of exceptions, and then they replace them by something even richer and much less structured.

I fail to see any pros of this proposal. It seems mostly equivalent to exceptions, i.e., you can easily express raise/try/finally as syntactic sugar, and likely the other way round. But it is (1) much more ad-hoc -- you cannot give a simple reduction semantics for this, (2) much less orthogonal -- tying `defer' in with function definitions, (3) dependent on mutation -- for recovering and returning alternative results, and implicitly in the semantics of `defer'.

One consequence is the loss of beta-convertibility, which means that you as a programmer cannot take an arbitrary piece of code and turn it into a function anymore, or vice versa, eliminate a function by inlining its body. Such abstractions/refactorings are impossible in general under this proposal, or at least require transforming the relevant code in potentially non-trivial ways.

Bottom Line

Google is hiring people to design languages and tools that know absolutely nothing about modern language design techniques, or what modern software engineering tools look like.

If you're paying any attention at all to Go, you're not learning anything. In fact, it's possible you're damaging your brain. It's projects like these that make Google look really bad and unattractive to programming language researchers, especially as compared to Adobe, IBM, Sun, and Microsoft.

[edit: well, I feel like an idiot. I just saw you joined Google last month. Good luck.]

Not quite fair

Counter-example: they hired Mark Miller.

I confess to disappointment with "Go". In operating systems, Rob and Ken are both noteworthy for their sense of taste. It is saddening to see that they haven't carried this over into language design. From what I've heard informally, reaction within Google to Go has been mixed.

In Go's defense, however, it may be a case of "least language sufficient to get started." Google operates in incremental steps. Successful languages, arguably, also operate that way. Getting a compiler for a systems-suitable language with a modern type system to actually build and function usefully is an overwhelming amount of work (as we have been learning with BitC). Given that they aren't in the language research business, Rob and Ken may have made a good decision by setting initially modest goals.

Also, they have clearly paid attention to a bunch of usability and "engineering scalability" issues. These kinds of issues are often dismissed by the PL community, but they are terribly important in real use.

From what I've heard

From what I've heard informally, reaction within Google to Go has been mixed.

This is not surprising, considering the high percentage of Mountain View workers with Stanford degrees. Those folks, while effortlessly brilliant, emerge from school thinking Bjarne Sjoustrup designed the world's last programming language.

It's true what Alan Kay says: perspective is worth 80 IQ points. Point of reference for "reaction within Google", is precisely that: you get insight, not outsight. Insight for Google is "this looks better than the C++ I learned from Koenig & Moo course at Stanford." And, again, pretty much everybody at Google I've met is effortlessly brilliant. They just lack a large enough body of evidence to make their observations meaningful.

edit: p.s. is Mark Miller designing a programming language at the Goog?

re: Mark Miller

safe ecmascript stuff, iirc, tho i'm sure he could comment here himself :-)

Let's try to keep this civil (and on-topic)

It isn't productive to stereotype any company or university as only hiring or matriculating people with a single ideology or worldview.

It certainly isn't relevant to a discussion at LtU.

Let's discuss Go on its merits, as we should any PL, rather than resort to ad hominem barbs.

after all, they do, in fact, for example,

sometimes teach Scala at Stanford.

[noted]

Tim,

I guess I was just thinking out loud. I have been wondering for awhile about where recent Ph.D. grads in PLT-work go after they've been signed-off on. For example, I saw Wadler's student Ezra Cooper is now working at a place in Boston doing XQuery stuff. I also notice how many people Microsoft Research hires. But I never really notice Google hiring anybody, unless it's to write compilers. Maybe that's just ignorance on my part.

FWIW...

...when you have projects the size of some of Google's, with teams the size of some of Google's working on them, Go's insanely-fast compile times become really, really compelling, justifying a variety of language design trade-offs necessary to result in them.

That doesn't mean that I'm a Go fan. I'm very much not. But it does seem worth bearing in mind the kinds of problems that Google has that Go might be very well attuned to.

Interesting objection

Wouldn't it be better to hire Ph.D.'s with language research background and focus on a totally different way to solve the problem other than simply typical batch throughput?

For batch throughput, I would hope that my dominating care is that the nightly clean build completes by the next work shift. For all else, I would expect more interactive builds, producing fast feeds to QA, to matter most.

Maybe my so-called ad hominem barbed wire attacks are simply a fundamental misunderstanding of what challenges Google faces*, and maybe Rob Pike's exceedingly long explanation linked above is a sign of deep thought, rather than clumsy english prose that will ultimately result in gotchas.

* Google, from Tech Talks I've watched, does a number of things I don't understand why they would do given their computing grid.

Yikes

Z-Bo: Maybe my so-called ad hominem barbed wire attacks are simply a fundamental misunderstanding of what challenges Google faces*, and maybe Rob Pike's exceedingly long explanation linked above is a sign of deep thought, rather than clumsy english prose that will ultimately result in gotchas.

I don't recall claiming any such thing, so I have to imagine that you're actually responding to someone else. :-)

With that said, all I was trying to do was suggest that Google might find some—and maybe a lot of—value in the engineering trade-offs that Go, wittingly or unwittingly, has made. From a PLT perspective, believe me, virtually every read of the Go blog I make results in a facepalm. It's just that that may be largely irrelevant from Google's perspective.

I was wondering what blog

I was wondering what blog this is, exactly. http://blog.golang.org/ only has a single entry. Do you mean Russ Cox's entries re Go?

Paul,

See above. I was referring to Tim's very fair criticisms directed toward me. There's no "yikes" involved, either.

I don't understand the "largely irrelevant" idea. Google should care about good design, like Microsoft does, even if they don't have MS Research's preposterously absurd research budget. I just noticed Andreas Rossberg joined Google, which is a step forward for them, but I suspect they need much more smart people with formal training in languages. They also hired Lex Spoon to work on GWT, which is also a good sign that Google hopefully recognizes ad hoc solutions designed by "software engineers" don't work. There is strength in numbers. Until then, Google does not appear to be able to get out of its own way, and this Go exception handling mechanism seems like an example.

So maybe I just don't understand big corporations like Google, or the Google way of doing things. But I personally salivate at the thought of (a) thousands of engineers, many which have Ivy League degrees (b) thousands of computers (c) in the same place. That a language should be used to connect (a) and (b) has always seemed obvious to me, and that you need people with formal training in language semantics to design that language also seems obvious to me. (e.g., I am certainly not the right person, as I would probably need at least 10 years more experience to be worthy of such an important position -- I know what I don't know, and it frightens me).

Design for what?

I don't understand the "largely irrelevant" idea. Google should care about good design, like Microsoft does

The last part of that caused me to laugh out loud.

However, I think Paul's point is the rather sensible one that "good design" can only be judged against the set of problems a particular design is trying to solve.

The problems that the Go team is thinking about may be problems that we just don't understand, or don't care about, but it's possible (though I won't estimate the likelihood...) that they have an awesome design for what they care about.

MS Research

MS Research is very serious about good design, and it's unfortunate you laugh at it. Microsoft is betting BIG that what will separate them from Linux in the future will be its research division, with projects like Singularity and Midhori. Also, if you pay attention to Microsoft's research initiatives in master metadata management, acquiring Stratature, etc. and folding Oslo into SQLServer, it is pretty clear Microsoft has a strong vision for how its research arm can help it build better products with smaller, easier to maintain codebases. Really, MS Research + MS engineering is a model for how it should be done. MS Researchers should be very proud of the environment they've created at MS. And guys like Michael Howard have done a good job convincing MS corporate that the endless-backwards-compatibility strategy hurts Microsoft's software quality over the long haul.

I'll relent that maybe Go has some set of goals I don't have in mind, but the reasoning is definitely not scientific. You can read the FAQ yourself to see the depth of thought and whether they measured things or whether they simply made decisions they hope to be true.

Black hat, white hat

MS Research is very serious about good design, and it's unfortunate you laugh at it.

I wasn't laughing at MS Research. It seemed funny to me to make a sweeping generalization about good design using two very large and diverse companies as poster children, but use Google, whose claim-to-fame is a very well-designed core product, as the bad guy, and Microsoft, who were widely regarded as laggards on any kind of coherent design for a couple decades, as the good guy.

Both organizations are so large and complex these days, I'm not sure I'd feel comfortable assigning either of them a uniform design approach or philosophy. I think this is likely to be a project by project, team by team consideration.

Endless backward

Endless backward compatibility is what sustains a profitable platform company (and languages are platforms). These are two sides of the same coin; you need to have the bravery to destroy your current product with a new better one, but you still need to maintain solid compatibility for decades at a time, even if you have to stick the previous product into a virtual machine and ship it with the new.

Personally, I'd prefer this

Personally, I'd prefer this thread returning to the topic of the OP.

I second this very strongly.

I second this very strongly.

I agree; it would be better

I agree; it would be better if people didn't insert random, apparently misinformed tangents into their posts. I'll only add that it's important to bear in mind economic motives when criticising companies' technical decisions. Ivory tower criticism doesn't help anyone.

They had a good idea with

They had a good idea with their implementation of interfaces (structural typing of sorts), but IMO they crippled it because you can't implement post-hoc interface extensions ala type classes: methods can only be defined internal to the module, and only methods are lifted to interfaces.

Suggested alternatives?

> It is ironic that their motivation is to tame and restrict the use of exceptions, and then they replace them by something even richer and much less structured.

Andreas, you make an excellent point. In your opinion what are some viable alternatives to exception handling for error handling? I ask because, I wonder if in my own language design I should be rethinking my default position of using exceptions. To be honest I am ignorant of any mechanism other than error codes, exceptions, and longjmp.

[EDIT: I should have added to the list asynchronous message passing].

No suggestion

I don't have any other suggestion to offer. From what I can see, exceptions in some variation are still the least terrible option. Sure, they can be misused, but I don't see how this particular proposal avoids that.

Error Handling Patterns

This is a quick list of error handling mechanisms:

Error Codes
One of the return values is the error. Use when the goal is to cover-your-ass by ensuring that error handling is possible, even though you know any error handling or recovery would clutter the happy-path and thus ensure programmers are reluctant to admit to their existence at all.

Product Types (i.e. (Result,Error) pairs)
Error codes, reformulated for language with efficient tuples. Same issues apply.

Ambient Error Value (Errno)
Error information is stored in some ambient space, such as a global variable, thread-local storage, or dynamic scope. This allows you to simplify the interface for the happy-path, and thus make it even easier than error codes for developers using your API to completely ignore error handling.

Null Object or Error Object
For OO. Constructors for common classes of 'error' are recognized in a superclass. I.e. "IDivByZero" might be a subclass of "Integer" returned after a division by zero. These objects report reasonably sane values and have sane behaviors for most queries and operations, to avoid crashes. The error can be recognized by extra interfaces of Integer that indicate whether the object is truly an error or not (and perhaps provide an enumeration for which class of error). Use this Error Object pattern if your goal is to ensure that an error in one part of the program will lurk around like a submarine, waiting for the opportunity to sink some other part of the program before vanishing, without a trace, into the bloody waters.

Sum Types (i.e. Maybe monad)
Return value distinguishes between success, and handling the different cases is enforced at the type level. Use when the goal is to force programmers to at least recognize the error, if not recover from it. A little syntactic sugar for a hidden variable can go a long way towards avoiding clutter.

Error Logging
Keep a trace of activities and errors so that, when when everything goes to hell, you at least have a record of the good intentions that paved the road.

Pass a Handler (Error Counseling)
Error-handlers are represented explicitly and passed into the program (via argument, or dependency injection, or dynamic scope, or thread-local storage). When the error occurs, call the appropriate error-handler with the necessary arguments. The error-handler either represents a continuation (in which case you call it from a tail-recursive position), or will inform you, i.e. via return value, of how you are to continue after the error. This is a flexible and powerful mechanism, and works well in combination with almost any other error handling mechanism (except for resumable exceptions, which effectively subsume this pattern).

Exceptions
Include a non-local exit so that error-handling may be isolated upon the stack. Unfortunately, in many languages this "unwinds" the "stack", and thus a lot of important, unfinished work is simply lost, and the error-handling policy cannot effectively say anything useful about error recovery.

Beware, also, that exceptions have a dire impact on parallelization and partial-evaluation (beta reduction), especially in the face of modularity. The main reason: unless you are also okay with going non-deterministic, exceptions force you to give exceptions a 'precedence' based on an evaluation order. Sum-type and Pass-a-handler does not have this problem. (OTOH, if you need a well-defined evaluation order regardless, then exceptions won't hurt you in this regard.)

Resumable Exceptions
The unfinished work is maintained during error handling, and there are mechanisms available to return to that work - i.e. by return value, or via 'resumption points' annotated in the code (which look the same as exception handlers). This allows a great deal of flexibility for what those error-handling policy can express, similar to the pass-a-handler approach. The greater structure, however, can lead to better performance and static analysis.

Usefully, the difference between resumable exceptions and regular exceptions only requires a very minor tweak in implementation, even in languages such as C++ and Java: handle the exception as a new activation at the top of the stack, rather than unwinding first. (These activation records would need to include a pointer to the original frame in addition to the stack pointer.) This is exactly what Dylan does.

Deferred Exception Object
Similar to Error Object pattern, except that the "sane responses to most queries" are replaced by "throws an exception for most operations", thus turning this object into a neat little grenade. Only a few interfaces will not throw an exception, such as checking whether the object represents an error, and which error it represents. Thus, you have opportunity to recognize and dispose of it properly rather than pass it into some delicate part of the computational machinery that doesn't expect to suffer concussion damage.

Transactions
The error didn't happen. (Handwave.) Nothing else happened either, making this a rather inflexible failure mode. Very powerful. Equivalently, very difficult to make this work at the interfaces between systems, i.e. with sensors and effectors, though an ad-hoc approach may be sufficient and XOpen/XA might help a bit.

Do or Die (Let it Crash)
If you can't complete, engage in a manic bout of parricide followed by suicide. The primary advantage of this approach is that dying early, and dying fast, is dead-obvious to the developers, and thus forces them to handle the problem rather than shipping it. (One hopes.)

Resilience and Self Healing
Build death and rebirth into the language or architecture. Erlang is well known for this style of error handling; it uses 'let it crash' along with supervisors for recovery. You need high-level resilience and cascading destruction because the high-level is where you have the most knowledge about how to recover from certain partial-failures without breaking your clients. Low-level resilience is useful for performance, but only when you can ensure that the recovery doesn't violate any non-local semantics.

I'm fond of sum-type error handling, pass-a-handler, and effective resilience (by minimizing need for state). I've gone through stages where, for a few years, I was enamored with resumable exceptions and later with transactions. But simplicity, modularity, and local reasoning are not well served by exception handling and transaction mechanisms.

In 'ZenOfHeron' you suggest that Correctness trumps all else. But most programs have external dependencies. A prerequisite to local reasoning about correctness in the face of dependencies is reasoning about how failures in a dependency might be exposed to your code, and how your code goes on to expose failures to its clients. I'm a bit surprised you haven't placed more research into the subject.

Great summary

Thanks for the thorough overview of error handling patterns David.

In 'ZenOfHeron' you suggest that Correctness trumps all else. But most programs have external dependencies. A prerequisite to local reasoning about correctness in the face of dependencies is reasoning about how failures in a dependency might be exposed to your code, and how your code goes on to expose failures to its clients. I'm a bit surprised you haven't placed more research into the subject.

You bring up a good point. I rewrote that point to be a bit weaker: "Code must be correct before it can be made efficient".

I have to confess that I am a bit hung-up on exceptions. However, I am rethinking this a bit because I plan on introducing support for asynchronous message passing into objects.

One idea I have been toying with a lot is implementing a plain exception-based system that sends a message containing both the catch block and the finally block as closures to a object registered as a handler. As you point out we lose the unfinished work, but I am not so sure that that is a bad thing. I have a hard time convincing myself of the value of resumable exceptions.

My view of exceptions (which may be flawed and limtied) has always been that it indicates either a simple local failure requiring some interaction with the user or to try another path of execution. Or it is a critical error in which case the subsystem needs to be isolated and restarted or closed.

So my thinking is that the regular exception handling is good in the first case, but that allowing object and module to first decide whether it is going to even invoke the catch block suggested by the local code can help when dealing with massive or unanticipated failures in sub-systems.

Utility of Resumable Exceptions

As you point out we lose the unfinished work, but I am not so sure that that is a bad thing. I have a hard time convincing myself of the value of resumable exceptions.

My view of exceptions (which may be flawed and limtied) has always been that it indicates either a simple local failure requiring some interaction with the user or to try another path of execution.

Generally, the code that experiences the error does not - and should not - know how to recover from it, which alternative path to take. Baking this knowledge into code will mean rewriting the code in a slightly different manner for each slightly different recovery policy. This does not scale very nicely. Thus, rather than baking error policy into the code, code that expects to see reuse (generic programming, frameworks, etc.) must abstract across error-handling and recovery policies.

I.e. consider two generic functions for collections: sum and product. And consider two orthogonal error-handling policies: ignore (skip an element) and abort (exception goes to client). Ignoring errors, the code for sum and product look as follows:

sum(C,P) { 
  var result = 0
  for x in C
    result += P(x)
  return result
}

prod(C,P) {
  var result = 1
  for x in C
   result *= P(x)
  return result
}

We assume that P(x) can experience an error.

To cover the above with regular exceptions, we'd have a couple options: either we have two versions each of 'sum' and 'prod', one each of which ignores errors and the other of which allows them to pass through, or we have three versions of 'P' (one that returns 1 on error, one that returns 0 on error, one that allows errors to pass through). With first-class functions or function-objects, we can do well by at least producing the extra forms of P on-the-fly.

And that's with just one layer of indirection from the primary caller. If the 'sum' or 'prod' functions are called from another generic function, things can explode in a combinatorial manner very quickly.

By abstracting over error-handling policy, however, we can avoid this combinatorial 'explosion'. There are many ways to do so. You can, for example, pass in a value (e.g. an enumeration) that selects your policy. Or you could pass in an object that can be queried for policy (which might really go all the way back to the user, and is also able to perform logging). Or you could use resumable exceptions. You suggest that errors may require some "interaction with the user" (or client) to decide the proper path for execution. Object-based pass-a-handler or resumable-exceptions mechanisms will provide that.

But any such abstraction will force you to work around the a 'regular' exceptions mechanisms, effectively rendering those mechanisms useless - or even harmful - for large regions of code. I do not consider regular exceptions to be a sane option for error handling in a general purpose language design. I think you would be better off going entirely without, instead favoring a pass-a-handler idiom, than you are favoring the non-resumable exceptions mechanism.

Resumable exceptions are reasonable for all the reasons one might favor regular exceptions instead of error-codes. They avoid clutter in the happy-path for execution. They provide discipline and structure, and something that may be clearly documented. They are easily analyzed for completeness. (They are what regular exceptions should have been all along!)

Excellent point

That is an excellent point about the explosion of error handling policies and is quite convincing. Are there models of resumable exceptions that allow the exception throwing code to prevent resumption? If I write a library, I may know that resumption on exception is a "bad" thing in a given context (e.g. design flaw) and I want to prevent the calling code from forcing a continue.

Yes

Yes. And I'll offer my general view of exceptions: I think it's useful to distinguish between error conditions that are describable at the current abstraction layer and those that break the current abstraction layer.

For the former, sometimes you still want to present these as out-of-band control flow. If you have sufficiently powerful control flow mechanisms in your language, then you can have an arbitrary protocol in place here. No special exception mechanism is needed. You can support resumption, non-resumption, or parameterize this behavior.

For the latter, reasoning about recovery from errors is much more difficult, and in general you just want encapsulated failure - a component is shut down from the outside, some intermediate results are probably lost, and some recovery action is taken (reporting error, starting up a new component). Assertion failures and out of memory errors are usually in this category. The choice of failure compartments is part of a program's high level architecture.

It's also nice that

It's also nice that resumable exceptions don't require special compiler support other than regular exceptions. You can implement resumable exceptions very easily on top of regular exceptions by passing error handlers through a dynamically scoped variable (these can be implemented easily in a library too).

Passing error/event handlers

dmbarbour wrote:

Pass a Handler (Error Counseling)
Error-handlers are represented explicitly and passed into the program (via argument, or dependency injection, or dynamic scope, or thread-local storage). When the error occurs, call the appropriate error-handler with the necessary arguments. The error-handler either represents a continuation (in which case you call it from a tail-recursive position), or will inform you, i.e. via return value, of how you are to continue after the error. This is a flexible and powerful mechanism, and works well in combination with almost any other error handling mechanism (except for resumable exceptions, which effectively subsume this pattern).

I generally like this approach, e.g. as used in parsers such as SAX (javadoc). My favourite approach (conceptually) to exception handling is to use essentially the same approach used to handle other kinds of events: i.e., a listener interface. You can apply this in Java (and other languages) right now, by applying a kind of CPS transformation (manually now, ideally automatically) so that code like the following:

public T foo(...) throws ExA, ExB;
try { T t = foo(...); } 
catch (ExA a) { ... }
catch (ExB b) { ... }

can be transformed into code like the following:

public interface FooErrorHandler {
    public void onExA(ExA a);
    public void onExB(ExB b);
}
public T foo(..., FooErrorHandler handler);
T t = foo(..., new FooErrorHandler() {
    public void onExA(ExA a) { ... }
    public void onExB(ExB b) { ... }
});

This is very flexible, as the error handler interface can be designed to allow e.g., returning a default value, or some signal to the original code on how to continue (e.g. "retry"). You can also abort the control flow by invoking some escape continuation. Finally, this also works better with asynchronous calls (especially in conjunction with futures). For example, while I love AliceML's use of futures and the "spawn" statement for converting synchronous calls to async, I don't like how exceptions are handled (exception is raised/handled where the future is requested, not where the original call was made). By passing an error handler as an argument, all the error handling logic can be concentrated at the call site in both the synchronous and asynchronous cases.

Ideally, this transformation would be done behind the scenes by the compiler to avoid having to explicitly pass around these error handlers.

Performance of passing error/event handlers

Why would you lower try/catch to explicit handlers instead of using stack maps? Consider the performance implications:

  • Each try/catch adds a new object to the stack, instead of the global read-only data segment
  • Each exception specification adds a new type
  • Each function adds a new parameter, increasing stack traffic and register pressure for every function in between the handler and the throw.

You've spent all of that performance, but haven't actually gotten much in return. Finding the exception handler becomes a little bit faster, but there is no change at all in the semantics of the language. The DWARF unwinder (and really, any stack map -based unwinder) is fully capable of implementing restarts. Unwinding proceeds in two stages. In the first stage, a handler is located. The handler includes some code that gets executed immediately (the language's personality function). If the personality function requests it, then the stack is unwound back to the handler and more code gets executed (the catch block itself, typically). The model was specifically designed to support restarts, by allowing the personality function to pass its "return value" through the exception object and direct the unwinder to resume.

Excellent!

The translation to Java interfaces and objects was intended to show the semantics, and demonstrate a simple proof of concept, not an industrial-strength implementation strategy. If stack-map based unwinders (of which I know nothing) can implement the same semantics more efficiently, then excellent!

RE: Performance of passing error/event handlers

Performance is a fair point. Specialized mechanisms, such as resumable exceptions, can be far faster - assuming the rest of the language cooperates.

Thus it might not make much sense to do the above in Java. But Neil also raised semantics issues for other languages, such as AliceML's 'future' primitives.

Dealing with stacks becomes ugly as parallelism and concurrency scale upwards. Once you start making decisions between 'stack-maps + cactus stacks' vs. 'continuation passing with dedicated handler objects', and all the resulting garbage-collection issues, the implications for performance become far less clear.

My own implementation preference - for clarity and simplicity, more so than for performance - tends towards continuation passing style with a dedicated 'nursery' per processor or OS-layer thread. I do not want to deal with 'stacks' even if doing so might allow me to eek out a few extra cycles. But I suspect the greater simplicity of CPS will gain me performance in the long run.

Exceptions + Futures

while I love AliceML's use of futures and the "spawn" statement for converting synchronous calls to async, I don't like how exceptions are handled (exception is raised/handled where the future is requested, not where the original call was made).

I fully agree that Alice ML does not have a totally convincing story regarding failure. But I don't understand what you are implying above: how could an exception possibly be caught at the point of the "original call"? The context of this call, and probably the whole thread that contained it, has long vanished when the exception is encountered. Also, you have to flag some error to a consumer of the failed future anyway. So as far as I can see, there is no real alternative to the way Alice ML combines futures and exceptions (short of removing one of the two features, which is probably what you are suggesting) -- except for killing the whole process whenever a thread terminates with an exception.

Edit: Part of the reasoning behind the semantics is that inserting a spawn is safe and does not change the meaning of an expression unless non-deterministic features are used.

Exceptions + Futures

Part of the reasoning behind the semantics is that inserting a spawn is safe and does not change the meaning of an expression unless non-deterministic features are used.

It is this part that I think is currently not quite right: inserting a spawn can change the meaning of an expression where exceptions are concerned. For instance, consider the following example:

exception Test
fun longRunning() = (* a long calculation *) raise Test
fun foo() = longRunning();
foo() handle Test => print("exception handled\n")

When you run this, you get the message "exception handled" printed. If you change the definition of foo to spawn longRunning(), then this message is no longer printed — our exception handler is ignored. Instead, the exception crops up later on when we request the future. Using the CPS-like semantics for exception handlers, the code would be transformed into the following:

exception Test
fun longRunning(handler) = (* ... *) handler(Test)
fun foo(handler) = longRunning(handler)
foo(fn Test => print("exception handled\n"))

This version works (i.e., "handles" the exception) in both cases. Note that here the future will be set to whatever the result of the exception handler is, i.e., () in this case.

PS - is Alice still being actively developed? It's a really inspiring language, which I'm seriously considering using as my main tool of choice for future projects.

Right

Right, I should have been less hand-wavy in my edit. There are a couple of extra constraints for this to be true, e.g. your computation must be sufficiently strict, and it may not install exception handlers itself. Arguably, that makes it a rather useless property in a rich language like ML. (FWIW, being able to sprinkle "spawn"s as a mostly semantics-preserving optimization in functional code was the original motivation for futures, as e.g. implemented in Multilisp.)

PS - is Alice still being actively developed?

I wish I could say yes, but there hasn't been any activity for quite some time, and I don't foresee any either.

how could an exception

how could an exception possibly be caught at the point of the "original call"? The context of this call, and probably the whole thread that contained it, has long vanished when the exception is encountered.

As a guess, capture a continuation at each such call which is carried along with the spawned computation. That sounds crazy expensive to me though, and I'm not sure what benefit it would provide. A consumer of a future value should be able to handle the return value or failure, if failure is a possible outcome of a computation.

Combining Futures and Exceptions

The two can be combined, but avoiding encapsulation-anomalies is important for modularity and security and safety. Valid options include the following:

  1. type-check which exceptions each future might throw, and prevent passing futures into code that cannot handle these exceptions. (Forces developers to wrap futures to adapt their exceptions, before passing them onward.)
  2. reduce all exceptions to a generic 'failure' exception, to prevent the future from accidentally releasing information about its source
  3. make futures second-class, i.e. by forbidding them from remaining futures during message passing (all futures must realize in-transit, or the message will never arrive).

There may be more valid options, of course.

The problem with these options is that they do not capture the exception handling and recovery policies associated with the continuation. Neil was not attempting to catch the exception "at the point of the 'original call'", but rather to package up and send the necessary context along with the future.

RE: Passing error/event handlers

I, too, am quite fond of the pass-a-handler mechanism. I like it because it is flexible and clean. By 'clean' I mean it avoids introducing any language primitives. I favor that sort of language minimalism, albeit less than I favor support for local reasoning about non-local properties.

while I love AliceML's use of futures and the "spawn" statement for converting synchronous calls to async, I don't like how exceptions are handled (exception is raised/handled where the future is requested, not where the original call was made). By passing an error handler as an argument, all the error handling logic can be concentrated at the call site in both the synchronous and asynchronous cases.

This is a good point. Raising arbitrary exceptions in whomever waits upon a future can be a serious violation of encapsulation, which would hinder both security and modularity.

I had initially tackled this issue in my own language by keeping 'futures' second-class. They can be referenced by name locally, and captured when spawning a process, but cannot be delivered as part of a message. However, this decision is all sorts of problematic if the goal is flexible abstraction and support for interesting communications patterns. I was recently reconsidering the decision, but I had long forgotten that security was among my original reasons for keeping futures second-class. Thanks for the reminder!

... Though, it seems to have very-recently become a moot point for me; my new language designs are reactive data-flow (and I'm experimenting with constraint management), solving many issues from message passing. Futures aren't very necessary in a language with temporal semantics.

Ideally, this transformation would be done behind the scenes by the compiler to avoid having to explicitly pass around these error handlers.

The notion of a dynamic scope can be tweaked just slightly to fit into a language with asynchronous message-passing. All you need to do is make that scope shallow-immutable, in the sense that 'updating' the scope only affects those you call in the future, as opposed to those who called you. Dynamic scope becomes a copy-on-write object associated with each continuation and message, and serves as an excellent place to drop parameters you don't want to pass around explicitly. A little syntactic sugar (macros, extensible syntax, etc.) can take that scope much further. It can be a powerful basis for extending or enhancing the language.

For security - i.e. to control confinement of untrusted code - an extra tweak is needed: the ability to grab the entire dynamic scope and treat it as a value, and the ability to replace the entire dynamic scope with a new value.

Though it isn't all roses. Message passing and procedural performance aren't much hurt by implicit context, because each activation of a message is a truly unique

That said, I rejected implicit context for functional programming, because it hinders all sorts of optimizations (especially for functional-reactive programming). Keeping things explicit encourages developers to narrow the interface, which greatly improves multi-cast. If I manage to get this constraint-reactive programming concept hammered out, I'll probably be abandoning implicit context (and dynamic scope) entirely.

Re: Error-Handling Patterns

Re: “a neat little grenade.”

The quoted appelation reminded me of Jeremy Brown's neat little exploding error codes.

I'm not sure I get the

I'm not sure I get the difference between sum types, checked exceptions (resumable or not), and pass-a-handler. It seems one can easily be transformed into the other via a straightforward transformation that doesn't really impact the clarity or composition of the code.

I also don't see how resilience is any better than checked exceptions. With exceptions, if you can recover from a local error then you can resume, and if not, then a top-level catch-all handler effectively kills the current computation and restarts it. Resilience doesn't even allow the possibility of local recovery though. As long as unhandled/escaping exceptions are inferred in the function's signature, I don't think this imposes undue burden on developers.

The reason checked

The reason checked exceptions don't scale isn't because of burdens on annotating functions; it's because it makes the exceptions thrown (an implementation detail) part of the API (i.e. at the interface level).

Exceptions, apart from their unwinding semantics, are blobs of data thrown up from the internals of the machinery below indicating why the machinery didn't work. They are an apology note, or a protest. It's not often that calling code can do anything meaningful with an exception without also abandoning the veil of abstraction the exception punctured through when it escaped. For exceptions like failure to open files or acquire other resources, probably the best thing to do is to show the error message to the user (where applicable, so that the user has a chance to fix the problem) or log it and generate an alert that an administrator can look at. Other exceptions may include validation of user data - in which case showing their message to the user is exactly the right thing to do - or internal semantic checks of validity of calling code, e.g. null pointer exceptions, in which case saving recovery info for the user's data and shutting down or restarting the process is probably the best way to go.

So checked exceptions are predicated on a premise that is usually faulty: that they encode information that the calling code can meaningfully act upon. Most of the time, that just isn't true, and forcing calling code to somehow guarantee that all cases are individually processed promotes poor exception handling style. And anyhow, the vast majority of applications are built using frameworks, where there will be a top-level handler, at the event loop or request dispatcher level; at which point the checked exceptions aren't really checking anything, since all cases are covered by default.

The reason checked

The reason checked exceptions don't scale isn't because of burdens on annotating functions; it's because it makes the exceptions thrown (an implementation detail) part of the API (i.e. at the interface level).

I already said that the checked exceptions would be inferred, so this is not a burden the developer has to pay.

Furthermore, I don't see why exceptions shouldn't be part of the API. Types are supposed to specify data flow and effects, like checked exceptions, specify control flow. The whole enterprise of typing is about specifying program behaviour as accurately as is feasible.

Integration and breaking changes

The burden comes from integration between third parties. Every change to an implementation that introduces a new exception changes the API, which in turn forces changes to all the clients. The only reasonable (i.e. future-safe) exception specification is that any routine can throw any exception; or checked exceptions that don't actually check and force third parties to handle such exceptions, such that checked exceptions purely provide documentation, perhaps through a symbolic browser.

I wrote a comment some ago on the specific case of Java's implementation of checked exceptions. It's not fully applicable here because inference of thrown exceptions removes a good chunk (perhaps 60%) of what's objectionable about Java's exception model, but something that's still relevant, I believe, is this mistaken idea that exceptions should be caught as a matter of course, and by implication that tracking whether exceptions are caught or not is a useful thing to do - most of the time.

That is, in the very rare case that you do want to catch an exception, exception specifications (perhaps provided by documentation and inferred from code analysis) are great. But it's very rare.

The burden comes from

The burden comes from integration between third parties. Every change to an implementation that introduces a new exception changes the API, which in turn forces changes to all the clients.

This is exactly what happens when you add or remove a new case to a sum type, or change a case's structure. Your argument applies equally to types, so are you arguing for eliminating static typing? If not, how are you differentiating between one type of checking and another?

That is, in the very rare case that you do want to catch an exception, exception specifications (perhaps provided by documentation and inferred from code analysis) are great. But it's very rare.

I can only speak from experience, but exceptions are rather pervasive in .NET, and working on web programs means I'm doing a lot of parsing of client input and network requests where failure is pretty common. Even in common UI code with simple callback-style event handling, sanitizing user input requires a lot of exception handling, and these are exactly the sort of exceptions you want to handle locally.

So local exception handling isn't all that rare in my experience. You could argue that these parsing functions should just return a sum indicating success/failure, but that's just a checked exception in disguise. I have no objection to using sums everywhere either, but I don't see much of a case for preferring one over the other, assuming inference for both is available.

Same domain, different approach

A few years back, I implemented a domain-specific dataflow-style data binding language as part of a web application framework, running on top of ASP.NET but not using much beyond IHttpHandler. Heavily data-entry oriented applications in the finance / insurance sector were built on top of it. So naturally, user data validation was a major focus.

But I cannot concur with your approach as described. Parsing user data that needed specific handling would be done with Xxx.TryParse(str, out xxx) rather than Xxx.Parse(str) methods; because when validating user input, errors in parsing are expected, i.e. they are not exceptional occurrences. One does not usually implement error handling in a parser with exceptions, particularly when you want error recovery.

You'll say that this is sum types or exceptions in disguise. I don't really agree though, because the stack "unwinding", such as it is, is very shallow, and the style of code fairly distinct. The equivalent in exception-style would be wrapping a try/catch around every method call, very much an anti-pattern.

On the other hand, the framework did support declarative constraints (using the same binding language, essentially boolean-typed asserts with error message metadata) on the data at a field and row level (the system was not OO, was actually fairly relational, with network requests handled on a transactional basis working on a disconnected data set); and more complex constraints could be expressed in C# (or other .NET) code. Such complex constraints could and did throw exceptions to represent a constraint violation. But such exceptions, once thrown, were not caught until they hit the framework again, whereupon they were converted into an appropriate error response for the client's AJAX request, so that the client Javascript could display the message associated with the exception alongside the problematic field(s) for the user to correct.

In other words: exceptions were caught by top-level handlers; the catching code did not do anything special based on the type of the exception (other than discriminating between a constraint violation exception that needed user display, rather than a system error exception needing logging); and checked exceptions would have added nothing (quite apart from being very hard to track through the tangle of DynamicMethod invocations).

This approach was somewhat influenced by my prior experience in the Delphi VCL school of exceptions, where the application message loop has a default handler that merely displays the exception in an error messagebox, rather than prompting a debugger like a .NET GUI app does. This encourages the use of understandable exception messages and a focus on exceptions as user-comprehensible transaction aborts at the UI operation level. And of course I work on the Delphi compiler now, so I'm still a fan of its cultural approach to exceptions. I'm somewhat frustrated in most other environments that - to my mind - get exceptions subtly wrong, either at the language or framework level. .NET comes closest, but it's not quite right in the frameworks by default, and it's not carried through to many of the exception error messages, which are too frequently user-hostile, and need catching merely to massage them, if nothing else.

Parsing user data that

Parsing user data that needed specific handling would be done with Xxx.TryParse(str, out xxx) rather than Xxx.Parse(str) methods; because when validating user input, errors in parsing are expected, i.e. they are not exceptional occurrences. One does not usually implement error handling in a parser with exceptions, particularly when you want error recovery.

Parsing a simple user inputs like integers in a typical callback-style event handling application is short, idiomatic .NET. I emphasize "short", because each event handler simply parses a single control value, so there's no need for sequential parsing where TryParse allows better structuring.

The control-flow of exceptions in this scenario is, as you said, "shallow" and equivalent to flat sums and TryParse methods. Furthermore, you claim try-catch around each method call is an anti-pattern, but given the close similarity in control-flow between TryParse and local exception handling, there must be some simple reasons why that is.

Firstly, it's because try-catch are statements, not expressions. Secondly, traditional try-catch syntax doesn't sequence/nest well because they don't properly structure the continuation they implicitly capture.

You can clearly see how checked exceptions don't prevent composition in Haskell by looking at the error monad. So this isn't a problem with exception handling as a concept, merely the implementation needs improvement. I don't see an inherent reason why non-local control flow is bad as long as it's well structured and statically checked.

In other words: exceptions were caught by top-level handlers; the catching code did not do anything special based on the type of the exception (other than discriminating between a constraint violation exception that needed user display, rather than a system error exception needing logging)

I use the same pattern, except when I can handle specific errors locally. I agree that most of the time, a catch-all at top-level is what will be used, but at least with checked exceptions we'll know when that will be needed, and when it won't. This can be as simple as a "exceptions escape" and "no exceptions escape", or it can be more specific, like Java, Haskell and more ambitious proposals.

Abstracting over exception specifications

The burden comes from integration between third parties. Every change to an implementation that introduces a new exception changes the API, which in turn forces changes to all the clients. The only reasonable (i.e. future-safe) exception specification is that any routine can throw any exception; [...]

The problem in Java is that the whole exception specification sublanguage is utterly incomplete. It is a very impoverished effect system that is not at all closed under abstraction.

Exception specifications are part of a type, so you need to have the same abstraction mechanisms that you need for types. That is, be able to (1) be generic over exception specifications, or parts thereof, (2) be able to name and export exception specifications, (3) have an algebra for combining exception specifications.

If a language has that done properly, then the alleged "third party" problem disappears.

Doubtful

For exceptions like failure to open files or acquire other resources, probably the best thing to do is to show the error message to the user

Giving the user the option to retry opening the file or acquiring a resource can save you a lot of time. And that's just one of the use cases for restartable exceptions. See Chris Double's A tale of restarts.

It's not often that calling code can do anything meaningful with an exception without also abandoning the veil of abstraction the exception punctured through when it escaped.

We'd really need quantitative evidence for your claim here. [And note that with the use of recovery protocols, abstractions aren't punctured.]

Self-evident to the point of tautology

I'm addressing the issue of checked exceptions. I agree that retries / restarts can be useful, but I think that's orthogonal.

Why calling code can't often do much meaningful with an exception: because a chief reason for using abstraction is to hide details of how the abstraction is implemented, but failure is a function of how it is implemented.

To the degree that this is false, either it is not an abstraction that hides implementation details, or the failure is not concretely described / represented by the exception thrown, and rather is hand-waved at; which will rather reduce its usefulness.

Even checked exceptions are

Even checked exceptions are difficult to compose. Consider wrapping one first-class function within another: it is unclear whether a given exception comes from the inner function or the wrapper, since the exceptions might overlap in type or structure. Consequently, generic programming is also difficult. Consider corner cases of one exception occuring while processing another.

Exceptions are also difficult to use in context of concurrency or laziness. Compare to sum types, where errors are in-band with the response and can be held in normal variables for later analysis.

Inferring exception types from the implementation is a bad idea, at least across abstraction or module boundaries, since it implies an abstraction leak. If you do use exceptions, I recommend you annotate your interfaces with all observable exception types. In practice, this quickly reduces back to sum types - minus effective semantics for generic composition, laziness, or concurrency.

Errors via sum types can be propagated implicitly (cf. MonadError or ArrowError in Haskell). This offers one advantage similar to exceptions, but merging errors this way is more explicit and flexible.

Resumable exceptions are less expressive than pass-a-handler, though this isn't clear if you only use handlers to express the same patterns as resumable exceptions. Consider alternative possibilities, such as merging handlers to get advice or policy from multiple sources.

Consider wrapping one

Consider wrapping one first-class function within another: it is unclear whether a given exception comes from the inner function or the wrapper, since the exceptions might overlap in type or structure.

The same could be said of a sum type returned by the wrapped functions. I don't quite see the problem with an exception thrown in an exception handler either. If it is a problem, you could rule it out in the semantics since exceptions are checked.

Exceptions are also difficult to use in context of concurrency or laziness. Compare to sum types, where errors are in-band with the response and can be held in normal variables for later analysis.

Haskell provides rudimentary checked exceptions via sums, as does the paper I linked above. I don't see why what you describe is an inherent limitation of exceptions so much as specific implementations.

I could maybe see a problem with exceptions for laziness, but even there it just seems that the exception specification becomes part of the lazy value. Ditto concurrency with futures.

Inferring exception types from the implementation is a bad idea, at least across abstraction or module boundaries, since it implies an abstraction leak.

For clarification, you mean that the programmer might miss an exception that he didn't mean to expose? I agree that interfaces require exception specifications, or at least must abstract over the exception specification of its implementations.

Sum types

RE: "The same could be said of a sum type returned by the wrapped functions."

If you assume `unboxed` sum types, where `a + b` is sometimes the same as `a` (e.g. where Int + Int = Int), then it is true that sum types might have several of the same generic composition problems as exception mechanisms. I assume something closer to the Either type. Given Int + Int, you still know whether you have a Left Int or a Right Int.

RE: "you mean that the programmer might miss an exception that he didn't mean to expose?"

Not just that. An exception might also carry information that should not be visible to clients of the abstraction, even via something innocuous like a string. Programmers need to be careful about which exceptions they mean to expose. Exceptions lead to enough subtle security concerns that I elide them entirely in my designs.

RE: "Haskell provides rudimentary checked exceptions via sums"

Sure, sum types with monads or arrows is certainly expressive enough to provide something similar in purpose and user experience to exceptions. But that is an insufficient condition for calling them the same. Even isomorphism is not enough.

RE: "even there it just seems that the exception specification becomes part of the lazy value. Ditto concurrency with futures"

That really doesn't meet the definition or vision I have for exceptions.

Perhaps you should settle on some precise definitions for your own reasoning so you don't have vague or moving goal posts for comparison. You don't need to use my definitions, of course, but your definition should be sufficient to distinguish between error codes and exceptions as used in practice today, or even sum-types and exceptions used in the same language.

Given Int + Int, you still

Given Int + Int, you still know whether you have a Left Int or a Right Int.

So your problem is the implicit unification of the same exception type from two different sources?

An exception might also carry information that should not be visible to clients of the abstraction, even via something innocuous like a string.

Do you again see this as a potential leak due to implicit unification?

But that is an insufficient condition for calling them the same.

I'm not calling them the same, I'm saying they're quite similar in their typical incarnations, and using certain implementations as an existence proof demonstrating that some of the problems you raised are not necessarily a problem with the concept of exceptions.

your definition should be sufficient to distinguish between error codes and exceptions as used in practice today, or even sum-types and exceptions used in the same language.

The only important difference between sums and exceptions seems to be the implicit unification described above. I brought up Haskell and that paper because I was trying to explain that once you have sums and first-class functions, you can reproduce the non-local control flow of exceptions. So the flattening is the only distinguishing feature.

As for lazy values/futures, I still don't see the problem. If you try to dereference a lazy value/future, it may throw an exception if the computation didn't succeed. The spec for a future should abstract over the set of exceptions thrown by the computation for that future. Once again, almost exactly as if the computed value were a sum.

The only meaningful difference is that sums in current languages tend to be require more explicit handling, and exceptions have certain implicit operations, like the unification. But sum operations can also be made implicit. Adding a sort of unification could be one such extension. Similarly, you can alter exception semantics to avoid the implicit operations you consider problematic and bring them closer to sums in their explicitness.

It's important to identify specifically what implicit or explicit operations impede composition. The possible semantics of sums and exceptions are flexible, not rigid, since those terms describe a class of abstractions, not specific abstractions. I don't see this position as being inconsistent or moving goalposts.

So your problem is the

So your problem is the implicit unification of the same exception type from two different sources?

That is one problem.

Do you again see this as a potential leak due to implicit unification?

It has more to do with visibility, awareness, and understanding languages as user interfaces. The implicit nature of exception propagation makes them a vector for information that is not readily visible in the source code. This can be countered by explicit annotations, at least for exposed interfaces. Cf. secure interaction design.

The only important difference between sums and exceptions seems to be the implicit unification described above.

Another important difference is that exceptions are (by my definition) embedded in the control flow in a structured way standardized by the language (or monadic context, etc.), whereas sum types are independent of control flow - they're just data and can be stored or communicated as such. There are important consequences of this difference, i.e. sum types are orthogonal to control flow and time, concurrency and reentrancy, whereas exceptions are not.

I'm not calling them the same, I'm saying they're quite similar in their typical incarnations

You'll find plenty of similarities between error handling mechanisms. It is important to find the differences, too. Corner cases are a good place to search for them. It won't help to focus on just use-cases where they are `quite similar`.

The implicit nature of

The implicit nature of exception propagation makes them a vector for information that is not readily visible in the source code. This can be countered by explicit annotations, at least for exposed interfaces.

I'm trying to understand specifically what information or implicit propagation is problematic and whether that is or can be addressed by checked exceptions, even in principle. What I'm trying to say, that's not enough to just say, "checked exceptions are bad, sums are good, m'kay?"

Whatever you consider "bad" may be a semantics that someone else may try to add to a sum calculus, eg. implicit flattening/unification, and wouldn't have even found your objections or thought them relevant because you just criticized exceptions.

It is important to find the differences, too. Corner cases are a good place to search for them. It won't help to focus on just use-cases where they are `quite similar`.

I agree 100%, which is why I've been saying that some of the properties of exceptions you've pointed out aren't "essential", eg. implicit flattening. The core "essence" of each error handling mechanism is what distinguishes it. Error codes describe cases with no additional information, sums describe cases carrying arbitrary amounts of additional information, and exceptions are a pattern for non-local control flow (whether implemented natively or monadically).

Exceptions themselves can be error codes or sums. The semantics of exception sums is orthogonal to the essence of exceptions. So I think finally we've reached the only essential problem you have with exceptions specifically: non-local control flow.

But checking is supposed to solve the control-flow difficulties associated with exceptions, so how precisely is it deficient? Is the checking you would require possible, even in principle? E specifically requires exceptions to be "deep frozen", ie. transitively immutable. Does that suffice?

As you know, there are many patterns for control flow, and delimited continuations can support them all as far as I know. So if someone invents a language with delimited continuations, I think it's important to know precisely the control flow problems that might be inadvertently introduced.

Contrasting exceptions and continuations

These days I think the more

These days I think the more important result is that *delimited* continuations can express all of exceptions, state, continuations, and indeed any monadic effect.

some of the properties of

some of the properties of exceptions you've pointed out aren't "essential", eg. implicit flattening

I am curious how you came to the conclusion that implicit flattening can be separated from exceptions.

Implicit flattening is inherent to exceptions due to how they are structured in code and implicit propagation. For example, assume function f can throw exception e. Call f twice. Rather than distinct exceptions `e + e`, we end up with just `e` unless we *explicitly* catch the different calls to wrap and distinguish the exceptions.

specifically what information or implicit propagation is problematic

Exposing an error that is due to the implementation (not essential to the abstraction), is by nature an implementation leak. An implementation leak can be acceptable, but it is important that they be visible to developers in a security sensitive scenario. Exceptions will hide the leaks from view (via implicit propagation). It is useful to counter this by explicitly annotating which exceptions may be thrown at each interface.

Rather than seeking a `specific` problem, this is more about feature interaction.

Whatever you consider "bad" may be a semantics that someone else may try to add to a sum calculus, eg. implicit flattening/unification

If people modify `sums` far from their category theory, then it is no longer clear that it is a sum.

the only essential problem you have with exceptions specifically: non-local control flow.

I'd say the problem is more the *coupling* to control flow, as this is also what causes problems when exceptions occur in exception handlers, in lazy code, or in parallel. The very notion of `control flow` has its own problems, being suitable primarily for systems without concurrency.

checking is supposed to solve the control-flow difficulties associated with exceptions

Checked exceptions, annotated at certain boundaries, do help developers confine exceptions to a volume of code. That can help with the implementation leaks, as I've noted before.

But they don't help with any of the problems related to composing systems with exceptions.

E specifically requires exceptions to be "deep frozen", ie. transitively immutable. Does that suffice?

Suffice for what? I'm not sure what problem that is supposed to solve.

It seems to me an unnecessary and limiting constraint. Several useful error-handling patterns for separating policy from recovery involve exceptions or other error values carrying `suggested continuations`, which in turn may encapsulate references to mutable state.

there are many patterns for control flow, and delimited continuations can support them all as far as I know. So if someone invents a language with delimited continuations, I think it's important to know precisely the control flow problems that might be inadvertently introduced

If you support all the patterns, you also support all the inadvertent problems of those patterns. Is this not obvious?

I am curious how you came to

I am curious how you came to the conclusion that implicit flattening can be separated from exceptions.

Checked exceptions + path-dependent tagging. Each exception then carries with it a tag indicating its origin, and you must catch the exception type + origin, or specifically state ambivalence about the origin permitting unification.

Exposing an error that is due to the implementation (not essential to the abstraction), is by nature an implementation leak.

I agree. But I'm not convinced it's an implementation leak that is really a problem. You claim that annotations at boundaries are useful, but if required, are they sufficient?

You claim later that they don't help when composing systems with exceptions. I take it you mean some abstraction could be parametric over the exceptions thrown, and then whatever code composes two different instances will have to the union of all implementation-specific exceptions thrown. Is this an accurate assessment?

But in this case, either the top-level code wants to handle specific exceptions with specific origins, or it doesn't and exceptions unify, ie. to log them. Do you have a specific case that doesn't fit this characterization?

If people modify `sums` far from their category theory, then it is no longer clear that it is a sum.

Simple sums, polymorphic sums, extensible sums, implicitly unified sums (the dual of records where field extension is idempotent). Which are the "real" sums? They are all sums with different semantics.

Suffice for what? I'm not sure what problem that is supposed to solve.

Leaking authority primarily. I've since come to understand that you're worried about it leaking implementation details, but I'm not sure I see the problem with that. Any effect or region system is going to leak implementation details. Callers may care about implementation details, and for those that don't, they can specify ambivalence.

This seems like a reasonable approach if you care about specifying program behaviour, but perhaps it's not your cup of tea because you're more focused on loosely connected systems.

If you support all the patterns, you also support all the inadvertent problems of those patterns. Is this not obvious?

Of course, which is why I'm asking for the actual problems of the core pattern, non-local control flow in this case, and not implementation-specific problems like implicit sum unification.

Any effect or region system

Any effect or region system is going to leak implementation details.

I don't agree with this, at least if you take it as "Any effect system by nature, leaks implementation details". I believe the Right (TM) Effect System of the Future won't leak implementation details -- depending on how you define "implementation detail" that may be a tautology. A good effect system is able to detect when some use of effects is purely local, non-observable, and can be hidden from the outside, so the details won't leak. The "best" effect system let you hide all non-observable effects.

Or maybe you meant that "in practice, all effect systems will have limited expressivity and will leak implementation details in some situations". I would be more tempted to agree on this.

By the way, the ST monad in Haskell is essentially a region system, and it is used *precisely* because it is able to "not leak" some relatively simple case of non-observable effects use.

I don't agree with this, at

I don't agree with this, at least if you take it as "Any effect system by nature, leaks implementation details".

I'm claiming that an effect system, like checked exceptions, would inherently leak implementation details to the same extent that types would leak implementation details. For instance, if a function accepts an unboxed Int instead of just "Num a", that leaks details about the implementation of that function, ie. the limit of the domain, the performance properties, etc.

The analog of accepting "Num a" for checked exceptions, would happen when the interface being implemented specifies only certain exceptions may be thrown.

Checked exceptions +

Checked exceptions + path-dependent tagging. Each exception then carries with it a tag indicating its origin, and you must catch the exception type + origin, or specifically state ambivalence about the origin permitting unification.

I see. Your proposed solution is to make the exceptions leak even more implementation details - i.e. implementation specific paths. Further, you must now maintain paths when maintaining code. You will likely require explicit annotations so that you can stabilize path names in spite of arbitrary maintenance.

Perhaps you should think about this some more.

You claim that annotations at boundaries are useful, but if required, are they sufficient?

Some discipline is required, too. If exceptions carry opaque data - like strings - they are still likely to leak implementation details.

You claim later that they don't help when composing systems with exceptions.

I understand the the composition and leak issues to be independent.

Simple sums, polymorphic sums, extensible sums, implicitly unified sums (the dual of records where field extension is idempotent). Which are the "real" sums? They are all sums with different semantics.

Don't forget dim sum. That's also a sum with different semantics.

I've since come to understand that you're worried about it leaking implementation details, but I'm not sure I see the problem with that. Any effect or region system is going to leak implementation details.

The leak itself isn't a problem. The leak being implicitly propagated, i.e. not obvious in source code, is a problem.

which is why I'm asking for the actual problems of the core pattern, non-local control flow in this case, and not implementation-specific problems like implicit sum unification.

You won't get far with that approach to reasoning, Sandro. You need a precise system, with precise definitions, before you can say precise things about it. By dismissing `different semantics` as a mere implementation detail, you're waving goodbye to some of your (and my) best tools for rational argument and discussion.

Non-local control flow is a rather wide generalization from exceptions as I understand them.

Perhaps you should think

Perhaps you should think about this some more.

You've given me no reason to. Implementation details sometimes matter, and when they don't, the client can abstract over them. I see nothing wrong with that position, and most languages adopt exactly this position. You define and work with classes by default, and interfaces only when you want additional abstraction. MLs infer concrete types instead of functors.

The leak itself isn't a problem. The leak being implicitly propagated, i.e. not obvious in source code, is a problem.

So do you object to continuations? Do you object to inferred type classes? Many things that are inferred are not obvious in the source code, so that's a rather imprecise criterion. Even assuming I accept the premise that we always want to force abstraction over implementation details, I don't think it follows that implicit/inferred values are inherently problematic.

You need a precise system, with precise definitions, before you can say precise things about it. By dismissing `different semantics` as a mere implementation detail, you're waving goodbye to some of your (and my) best tools for rational argument and discussion. Non-local control flow is a rather wide generalization from exceptions as I understand them.

I do have precise definitions, and in fact, the essence of each error handling technique are orthogonal to the others, where yours are not. If you have a problem with implicit sum unification, then discuss the problems with that. If you have a problem with non-local control flow, then discuss the problems with that. If you have a problem with the composition of the two, then discuss that, but don't then dismiss exceptions that don't compose the two.

Frankly, I think that position is more precise than your approach since I'm not conflating orthogonal concetps (re: dim sum, snark is not an argument). As for non-local control flow, it's the only orthogonal property that is associated with the whole class all possible exception systems as I conceive them, though el-vadimo makes a good point that others have taken different views on it and made the definition even wider than I would.

So we're on the same page, when I say "non-local control flow", I mean specifically the idiomatic stack-unwinding control flow pattern. Obviously delimited continuations implement a far more general type of control flow.

Feature interaction and intersection

That you fail to grasp or acknowledge that my concern is one of a feature interaction - e.g. implementation leaks WITH implicit propagation, not either of those independently - is quite frustrating. I've explained it three times now, yet you're still attempting to isolate my concerns to a single feature.

Implementation details sometimes matter, and when they don't, the client can abstract over them. I see nothing wrong with that position

The problem with exposing implementation details implicitly: the resulting entanglements are often subtle and raise the costs for maintenance, extension, and upgrade. They also make it difficult to reuse code from a project without dragging along a lot of dependencies.

As you point out, many existing languages enable such entanglements, even require discipline to avoid them. I shouldn't have to point out that they also have well known, predictable problems on account of doing so.

the essence of each error handling technique are orthogonal to the others, where yours are not [...] I think that position is more precise than your approach since I'm not conflating orthogonal concetps

Error handling techniques should be defined by their historical and ostensive use, not by someone's idealistic obsession with isolating distinguishing properties. It is absurd to expect that patterns built over years by humans would not overlap or conflate concepts. It is idealism and arrogance to reject a definition on account of its essence being a conflation of concepts. You blind yourself to implied feature interactions by acknowledging only the unique elements. Working at your definitions to achieve an orthogonal essence is merely hubris and fallacy.

One example is worth a thousand words

An code example written with exceptions and the same example written with whatever you propose instead should be able to clarify what you mean.

What am I proposing? I don't

What am I proposing? I don't recall proposing anything.

Sandro asked about my concerns regarding `exceptions` while knowingly and persistently refusing to use the term as I did when I expressed concerns. That sort of ideological conflict is not the sort to be cleared up by example.

Sandro asked about my

Sandro asked about my concerns regarding `exceptions` while knowingly and persistently refusing to use the term as I did when I expressed concerns.

Despite your resistance to discussing the orthogonal concepts at play, I eventually clearly identified the actual problems you have with the semantics of checked exceptions, so no one has to simply accept your unjustified claims. Only one of your objections remains (implementation leakage -- which I'm not convinced matters), so checked exceptions without the problems you claimed are now possible.

I'd call that a win and I'm sorry you disagree.

You cannot define problems

You cannot define problems out of existence. Those problems remain for many exception models even if they are not individually `problems with all possible exception models` in accordance with your philosophy and the definitions you favor.

That aside, I am unconvinced that you've accomplished even your agenda of arguing an orthogonal element to be the `essence` of exceptions.

Your proposals for handling lazy and parallel exceptions actually fit the pattern of `deferred exception object`, which is not the same as `exceptions`, having vastly different control flow and handler selection characteristics and blocking potential support for resumption. You completely ignored one of the two composition issues - exceptions from inside handlers. Your proposed `path-based` annotations are certainly not orthogonal to modularity, which was among the concerns I initially expressed.

If your goal is to argue that "checked exceptions without [any of] the problems you claimed are now possible", then arguing that each issue can be solved separately is logically insufficient.

If you want to call that a win, I won't stop you.

That you fail to grasp or

That you fail to grasp or acknowledge that my concern is one of a feature interaction - e.g. implementation leaks WITH implicit propagation, not either of those independently - is quite frustrating. I've explained it three times now, yet you're still attempting to isolate my concerns to a single feature.

Sorry, but you did not state this clearly and unambiguously, hence my questions. Every example of a problem you gave was either specific to one or the other abstraction, or you spoke in such general terms that your precise concerns were not at all clear. Since hubris was raised, perhaps you shouldn't assume you are as clear in your exposition as you think.

I believe we now understand each other with regards to exception handling. As for charges of idealism, I again disagree that historical precedent necessarily trumps refining definitions as knowledge of the essence of abstractions improves. Language is constantly evolving, but I don't expect we will agree on this point since it's at the heart of many of our past disagreements.

Alternative: Uniqueness typing for comments

The main idea of this proposal seems to be that by tying exceptions to dying functions, it will be harder to use them, discouraging their use in ordinary situations. An obvious alternative would be to require a lengthy comment explaining the need for the exception mechanism in each case where it is used. Determining the ideal minimal comment length could be done via the sort of data mining that Google excels at. The obvious problem with this approach is that programmers might try to circumvent it by cutting/pasting comments or with repetitive "asdfasdf" style comments. To me this suggests that maybe what's really needed is some sort of uniqueness typing for comments.

exceptions ⇏ stack unwinding

Re: comment 68825 (I know I'm posting this reply outside of the “You won't get far with that approach to reasoning, Sandro” subthread):

Non-local control flow is a rather wide generalization from exceptions as I understand them.

⧺1 to that. That said, I'd be curious to learn if you take a much narrower view of exceptions than the one painted in A Study of the Applicability of Existing Exception-Handling Techniques to Component-Based Real-Time Software Technology by Jun Lang David Stewart (ACM Transactions on Programming Languages and Systems, Vol. 20, No. 2, March 1998, pp. 274–301).

If I may, a few quotes to give you the flavor of the paper:

We classify exception-handling mechanisms into two categories: programming-language-based and operating-system-based.

Programming-language-based methods typically cannot handle all exceptions uniformly [Lindsay 1977]. For example, if an error occurs in a system call or by code written in a different language, the exception cannot be caught directly by the language’s facility. Therefore, the error cannot be handled in the same way as exceptions raised by the language’s own mechanism. The programmer is forced to use the method provided by the operating system or other language to detect and handle the exception.

Here's another one:

Handler determination is the process of receiving the notification, identifying the exception, and determining the associated handler. Methods used to determine handlers and to propagate exceptions vary greatly. They can be divided into the following categories:

  • Stack unwinding
  • Handler pool
  • Combination of stack unwinding and handler pool
  • Backtracking exception identifier bindings
  • Scanning instances of objects

(A note in the margin: It seems to me that most people commenting on node 3896 equate exceptions to stack unwinding, with restarts thrown in as a useful, but uninteresting, generalization of the latter. In contrast, the quoted paper offers a much wider view of exceptions.)

Finally:

Handler scope is the entity to which an exception handler is attached. Depending on the exception-handling mechanisms, an entity can be a program, a procedure, a block, a statement, an expression, an object, or data. The handler scope can be divided into three categories: local, global, and hybrid.