Nearly all of the devices you use employ these approaches in one form or another. A watchdog timer[2] is a pretty simple and powerful version of this. Timeouts and retries follow a somewhat similar approach, Erlang just embraces that across the whole language. It really is a fascinating approach to a different design space(latency and reliability over throughput) through the requirements a telecom stack necessitated.
"Let it rollback and retry"
The way one thinks about processes in Erlang is different than how one thinks about threads in most languages. The expected behavior when you kill a process is that it will be right back up with a known good state very quickly, and you won't have to do much about it yourself because it's handled in a supervisor far from your local process. It's subtle, but it makes a huge difference.
In most languages one expects a thrown unhandled exception to wreak havoc. But in Erlang graceful failure and restarting is the norm, not the exception. It's the expected behavior.
Moreover, the responsibility of maintaining the process tree integrity is delegated fully to specific processes. "Business intelligence" (to ape a phrase) nodes are very effectively isolated from having to care. If they don't know how to handle such a restart, you just let them crash/be killed and restarted too.
This phrase has always thrown me for a loop in the context of most web development.
Mainly because if an application were coded in a way where it's crashing chances are it's never going to get itself back into a working state.
For example if your web app throws a 500 because your code is syntactically invalid or is doing something wildly wrong it doesn't matter how many times you restart the web server or spawn another process, it's not going to work. It's going to fail until someone updates the code base to fix the human mistake.
Most modern web frameworks can also handle the case where the /oops URL throws a 500 but the home page and everything else works. One page throwing an exception doesn't bring down everything.
Now if you're talking about things like retrying a database connection at startup until either a timeout hits or the DB becomes available, that type of stuff is very useful but this is something I've seen included in a lot of web frameworks in a lot of languages. It's essentially a few line while loop that looks for a specific type of exception and then calls the connect function until it works or times out.
In general I find in Elixir you're also dealing with error handling on a per function basis because it's common practice to do the ok / error tuple pattern. This is defensive programming to ensure you have an understanding of the system you're developing, just like you would do a try / except in other languages.
For example if you were doing token based authentication you'd want your function to return ok and the data you want when it successfully verifies the token but you'd also want to handle the 2 failing cases individually, one error / message for when the token expired and another error / message for when the token was tampered with.
"Let it crash" is not about syntax errors, it's about unexpected (ie, exceptional) errors, often as the result of a user taking a completely unexpected path in a large system that was never thought of by programmers (it's probably about more than that but I'm a BEAM n00b). It's happened plenty in web dev for me where a production worker crashes and it simply requires a restart or to be reset back to a known state because the user did something unexpected.
As for tuple return in Elixir, that is simply doing it wrong if it is being used for defensive programming (and the antithesis of "let it crash"). It's meant for handling known errors and makes for a concise way of dealing with with it in the functional world—it's similar to Go's multiple returns.
e.g. compared to OO (they are both pretty clean by me)
# rails
foo.update(params)
if foo.save
do_something
else
handle_error(foo)
end
# elixir
case Repo.update(foo, foo_args) do
{:ok, updated_foo} -> do_something(updated_foo)
{:error, error} -> handle_error(error)
end
It's otherwise very common for an elixir function to return a bare value if it's expected to always work (and "let it crash" if it doesn't).A lot of copy paste here from Joe Armstrong's thesis:
Worker process does the job. Another process, the supervisor process, observes the worker. If an error occurs in the worker, the supervisor takes actions to correct the error.
1. There is a clean separation of issues. The processes that are supposed to do things (the workers) do not have to worry about error handling.
2. We can have special processes which are only concerned with error handling.
3. We can run the workers and supervisors on different physical machines.
4. It often turns out that the error correcting code is generic, that is, generally applicable to many applications, whereas the worker code is more often application specific. -------
1. Exceptions occur when the run-time system does not know what to do. 2. Errors occur when the programmer doesn’t know what to do.
The basic idea is: Try to perform a task. If you cannot perform the task, then try to perform a simpler task.
To each task we associate a supervisor process- the supervisor will assign a worker to try and achieve the goals implied by the task. If the worker process fails with a non-normal exit then the supervisor will assume that the task has failed and will initiate some error recovery procedure. The error recovery procedure might be to restart the worker or failing this try to do something simpler.
Thesis: https://web.archive.org/web/20041204143417/http://www.sics.s...
The "let it crash" is referring more towards letting the specific operation you're operating under (i.e. a single web request, a scheduled job, an async method, etc.) crash, and let the thing supervising it handle the restarting of it.
Processing a request and running in to a "let it crash" scenario doesn't mean bringing the whole thing down, it means the individual request crashes, the web server supervising it catches the crash, sets the connection to have a 500 status and returns, and life goes on as normal, the rest of the process being unaffected. In other similar scenarios the supervisor may want to restart the process after it crashes, or do something else in response.
In general the ethos of erlang(/elixir) is to let things crash but more importantly have the process supervising it expect things to crash and know what to do next to recover from the crash.
It may not be a novel idea nowadays (especially with web servers where presumably every implementation among every language will catch exceptions and send a 500 error), but it was a design principle when Erlang was being made, and it's baked in to the language and runtime in a fundamental first-class way where every Erlang/Elixir application you run is a supervised tree of processes that care about what to do when a process under it crashes.
> Would it be better to bring the whole server down and have the operating system restart it?
It depends entirely on how you've structured your system. Some systems attempt to not-crash, but then cause repercussions on downstream dependents by attempting to continue when they shouldn't have. In those cases, it's better to just give up and die rather than erroneously continuing.
I'm not an Erlang user, but I _think_ the ideology is more along the lines of accepting the certainty that you haven't and won't account for all failure scenarios and embrace crashing as an inevitability and working backwards. You know it will certainly crash, so become really good at recovering from a crash.
Basically servers/services that don’t crash don’t exist. There’ll always be bugs. So instead of trying to fix all bugs, write code that can will gracefully fail and easily recover.
Simplified example, but if part of your app is a parser, you can write functions that parse known tokens. If the input contains bad data, your parser will crash. Its supervisor will detect this and be able to restart the parser,and can pass an error back to the user.
Now, should you add on code that handles known errors more gracefully and provides more info? Absolutely, but you don't have to, and imo that enables a very pleasant programming experience
The Erlang application/service should not be crashing. A crash happens in a process in a tree of supervisors and each supervisor can have strategies to recover if its child supervisor/process fails.
Supervision in Erlang/BEAM is not just microservice infrastructure with restarts. It handles many types of failures knside the application and only if all mitigations repeatedly fail would we crash all the way up to the trunk of the tree and crash the application. If that happens the Erlang heart should be starting it back up or otherwise invoking some last-ditch mitigation. Or if you run Erlang in a container, that's when you'd have your infrastructural stuff restart it.
Supervision in Erlang is much more granular and not about restarting the entire service.
I think this gives a good view of Erlang resiliency: https://youtu.be/JvBT4XBdoUE