Let It Crash the Right Way (2009)
mazenharake.wordpress.com
mazenharake.wordpress.com
Think about a very dumb user application like Word. Let's say word crashed with a Stacktrace, some log messages, the best error message ever, maybe even saving the user data in a backup file somewhere. The user will think Word is unusable.
Now let's consider a scenario in which Word recovered under the hood by losing the users data but never losing the GUI. The user still thinks it's bad, but he won't think it's unusable. To some degree he will blame himself for not saving the data himself.
That's why I think I would be way less willing to crash in front of the user and would apply Pokemon Error Handling (Gotta Catch them All) in all user facing software as a feature, not a bug.
Program gets bad or unexpected input (parsers, tokenizers, renderers, data processing): parse or process as much valid input as possible, then set a helpful error condition and return.
Program encounters a resource constraint (malloc failed, can't get a lock, no socket available, permissions problem): set a helpful error condition and return.
Something really went bugnuts (kernel panic): crash, but as gracefully as possible.
Raising exceptions is an OK way to transmit errors up the call chain, although it's not my personal favorite, but allowing unhandled exceptions to find their way up the call chain doesn't seem like a great idea to me.
edit: I went back and re-re-read the post. His points here seem reasonable:
> Do I know how I’m supposed to handle an error here?
> If not, then should I handle it? (Thus going back to specification)
etc., but I don't understand the code example at the end; I'd think it would be good to have more precise information about what failed and where and why, rather than omitting that exception handling and letting a parent process or something else deal with it further up the chain where the cause is far more opaque.
Depends on the situation. For instance: I usually do thorough checks on validity of arguments etc. If invalid/null ptr passed/etc (which then means there must a bug somewhere) some specific exception is thrown and this makes it all the way to the global unhandled exception handler which then take appropriate actions (dialog, log, kill application). None of the callers which are passing the invalid argument along know how to handle the error else the error wouldn't occur in the first place. I'm not sure there is a more appropriate way to handle this in eg C#?
Return to what? If malloc has failed then it's quite likely that large areas of the program can no longer work. If you get a lock timeout then your program is deadlocked and cannot continue.
The distinction that matters is anticipated errors versus unanticipated errors, and the size of the middle chunk of "anticipated, but rare and we don't have time to care about". If you have an unanticipated error you're in the land of unknown unknowns. At that point you have to decide whether it's more important to try to preserve unsaved program state (which may be corrupt) versus the chances of further wrong action (corrupting persistent storage, bad output, physical or electrical damage in some systems).
This is where "continuous saving" and journaling-orientated persistent data structures come in handy. You can crash the app and recover its transient state from persistent state without losing anything.
But, it's an Erlang process; it should be supervised (or separately linked or monitored, but that's neither here nor there). If it is, it will be restarted in a known good state (or at least; should be. That's the guarantee the developer needs to work towards in an Erlang process).
So given that, the outcome is that you get a log of what happened (and it stands out), you wrote less code, and you return to a known good state.
The outcome of the catch all is you have to explicitly log what happened, it is very easy to miss, and your process/system is left running in a possibly ambiguous state (since either that message should have been handled, or it never should have been sent).
Now, that said, dealing with bad user data and sending them a useful error message -is- something that requires some work, some sussing out of what went wrong, yes.
Talking to external resources (as alluded to elsewhere in mentioning jlouis' Fuse library) -is- something you want to limit crashes on (since supervisors are built as a hierarchy, and just because a piece of hardware is temporarily unavailable, you shouldn't crash so often that the system comes down).
Malloc...you probably still want to crash on. Crashes will percolate up the supervisor hierarchy, until eventually hitting the highest level one, effectively dropping all memory and resetting to a good state. If that doesn't get you to a good state, there -is- no way to get there without manual intervention.
Enough of this and it will crash the node. You need to design for this in an Erlang system.
The ever-helpful jlouis has some useful writing on the subject: http://jlouisramblings.blogspot.it/2010/11/on-erlang-state-a...
As well as these: https://github.com/jlouis/fuse
Sadly, this is not discussed as much as it should be in Erlang land.
You've never done user support, have you? Almost everyone would prefer "UI freaks out for a bit, restarts with no lost work" to "invisible loss of work".
Edit: I've just remembered while I was arranging my mortgage, the customer rep was using some sort of Windows native LOB application. It popped up a failure dialog box with some hex in it .. which she closed and carried on. Users don't care about quality unless and until it stops them doing what they're doing or loses work.
Which is exactly what Word does. https://support.microsoft.com/en-us/kb/316951
You still run the risk of losing state internal to the process if it's not persisted, and as a dev you have to deal with that, and ensure that when a process restarts it restarts into a good state, but it proves a really effective model.
Because it does exactly what the article says: You experience some unexpected system state, stop what you are doing, raise an Exception with an error type and error message and let a higher level element do something about it, which might include a restart of the subsystem if so desired.
Erlang has the idea of exceptions. You can explicitly throw them, as well as certain situations generating them. They can be caught, using the familiar try/catch paradigm (well, somewhat familiar; it's used a little differently, with a slightly different syntax, but basically the same purpose).
It also has the idea of an exit signal, which will kill a process regardless (unless that process is trapping exit signals, because you want to do something prior to exiting, or prevent it, or whathaveyou).
A crash is what happens when you don't catch the exception/exit. Same as in other languages. -But-, in Erlang, it only kills the lightweight process (the term 'green process' has been used, akin to a green thread, except no state is shared between them), rather than taking down the VM.
In Java/etc, when you have an exception, if you don't catch it, it percolates back up the stack to the top and takes down your application; the execution of -everything- is ended. You might have a watcher process that restarts the app, but you took out the whole app. In the event of an unexpected exception, then, you either killed the app, or have a catch(Exception) somewhere, which is widely viewed as an antipattern, and which you really don't know how to recover from.
In Erlang, when you have an exception, if you don't catch it, it merely kills the (green) process, which if not intended to be ephemeral, should be supervised, linked to any process it implicitly shares state with (linking causes a dying process to send out exit signals to the linked process, so they die and restart too) and have a well written initialization step that will read from any persisted 'good' state, and bring it back to that state. So you get back into a good state having only taken down the bit that actually went wrong, rather than everything. The other processes are unaffected; no memory was shared between them, so they can continue on their merry way. They might have been expecting a message from the process that died, but if so, they'll be waiting on a timeout, and should either retry sending it, or themselves die and restart (message passing makes no guarantee of delivery, so you have to deal with the possibility that messages won't arrive; this is really, really useful in distributed scenarios, as it means there is no difference in sending a message locally, and sending a message remotely).
In the Java case, you try your best not to let exceptions go uncaught, because the user experience is terrible if you do. But it's easy to miss things that can cause exceptions. And when you catch them, it may not be clear what to do with them anyway, so you end up writing a bunch of error handling code runs the risk of keeping you in a bad ambiguous state, that leads to hard to debug issues further down the line.
In the Erlang case, you treat everything as being able to die at any time, making sure your supervisor hierarchy is sensible, and making sure every process reinitializes properly in the event of its death. After that, you worry about catching only those exceptions you expect to happen and have a well defined way of dealing with (which is usually very few); any other causes the state to be dropped and that process to be restarted. You get a log of it, you can decide you want to explicitly handle that case, because recovery from it is well defined, or you can just shrug and move on because it truly was just a hiccup, and you don't need to address it; either way, the process is back in a good state, and the system is able to continue along with barely a blip.
EDIT: A few links to help explain it (as it's a different programming model than Java/C++/Python/etc, and understanding how the philosophy applies requires understanding the model) -
http://www.slideshare.net/dnene/actors-fault-tolerance-and-o...
http://stackoverflow.com/questions/3172542/are-erlang-otp-me...
What the article is getting at is adding an error handler that's not in the specification keeps the system in an unspecified state [or transitions the system into a different unspecified state depending on how you look at it].
Well, I guess there's another piece of context: it's probably best to treat systems as algebraically closed in that combining two systems produces another system and so forth [1]. This means that when a specific system crashes (and "crashing" is not a technical term in the context) the larger system may be designed to handle the error. With proper encapsulation, there's no way for the crashed system to "know" what's going on above it or what's best...without a specification.
In other words, the assumption for such systems is that if a system is supposed to handle an error then the specification would say so. High reliability fault tolerant systems aren't built with cowboy coding. The software expresses the design and while some systems are such that failing results in failing to delight someone with a piano playing cat, other systems are such that when they fail 911 calls are missed and people die. In those cases, the author is suggesting when you don't know, don't guess.
[1]: 52. Systems have sub-systems and sub-systems have sub- systems and so on ad infinitum - which is why we're always starting over. --Alan J. Perlis
The problem space which I like to add is the scenario in which the higher level system is a human.
In a sense humans are like old code, they do odd things, are unable to specify their expectations and sometimes prioritize expectations that are utterly wrong. But we as smart programmers, who want to succeed with our program, can not be satisfied with "scientifically correct" code. We need to produce code that humans use. Therefore we can not always crash when entering unspecified states. That's not "cowboy coding", that's handling the imperfection of the higher level system. Sometimes the additional error handler is good, because the crash itself is worse than an undefined state.
You'd want the UI to not crash, and show a nice error message:
http://joearms.github.io/2013/04/28/Fail-fast-noisely-and-po...
http://www.erlang.org/download/armstrong_thesis_2003.pdf
Specifically, the bits about "Let it crash".
Because the kernel code cowboys ignored the UX city slickers, the system failure is in a sense, double. Not only does it stop letting the user type words into boxes, but then it also fails by presenting the user with a bunch of useless gobbledy-gook. It would have been better if they had asked the people working on the higher level abstractions, "Should we handle <this type> of error and if so how?" Or with wonderfully non-technical ambiguity, asking, "What should we do when we crash?"
If the error is handled locally, there's no chance that a higher level can handle it more gracefully. What causes a low level system programmer to freak out, may be nothing if the higher level system is redundant in regard to the low level system. A system shouldn't assume it knows what is catastrophic in the large. A message stating, "I have an error" is the place to start.
So, Wings3D is actually written in Erlang. It's a quite nice and minimalist 3D editing program, and it behaves exactly how you describe: some operations may cause a crash, and Wings lives and your model dies.
This is not a large consolation.
— Ed Catmull, President of Pixar
https://signalvnoise.com/posts/2440-we-all-know-the-saying-i...
It does seem like a lot of people are afraid to let a user see an error, but if you're not extremely careful then attempts to shield the user can mask bugs.
I would like to accumulate a "stack trace" of calls among actors to see what led to a particular exception, and log it.
run() ->
B = spawn(fun() -> receive_func() end),
A = spawn(fun() -> send_func(B), io:format("A completed normally", []) end).
send_func(Pid) -> Pid ! whatever.
receive_func() -> receive _ -> throw(blah) end.
You will receive a call stack listing only an error in receive_func. You will not have the anonymous function in B (due to tail call optimization) in the call stack, and you will not have the send_func in the call stack, at all, because that process completed normally; there was no error involved with it (a fact you can see by the io format call, which will be invoked just fine). A sent its message, and then printed something, and that's it. B's error does not relate to A; A might have a separate error (such as if I were to add a receive clause to send_func, expecting a response, that times out after a while), but that error is unrelated to B, and will create an entirely separate error in the logs, with an entirely separate call stack.If you do have to know what happened in A then you are using messages where you should have used function calls, after all a message should be context free once it leaves the sender. This may require some re-thinking of the concept of messaging, it is wrong to see a message as some kind of RPC mechanism, a message is a self contained package sent to a recipient that contains everything the recipient needs in order to process it and/or elicit a reply. Any other state should be elsewhere and should not be required at all to identify the cause of the crash.
Getting this right can be quite tricky.