My understanding is that because elixir uses the beam vm it has per process isolation which is basically used to guarantee that the whole server will not crash on any one request. As I understand it, this is an advantage over what a framework like Rust’s Axum offers, is this correct? How significant of an advantage is this really over presumably just restarting the whole server? (I get that there is a performance tradeoff for gaining this robustness).
This is comparing apples with oranges. Rust is all about preventing crashes. The BEAM is about allowing for crashes by isolating them as best as possible and letting the system as a whole stay up even in the face of things going sideways in a context, where things going sideways by definition cannot be prevented – distributed systems communicating over networks.
It’s not that, but there simply is not general answer to your question. What even is “crashproof”? I can build you a server, which never gets any work done e.g. just returning a 500 on any request, but their process never stops. Is that “crashproof”? Probably depending on the definition. Is it useful? Not at all.
I’m not sure what do you mean by that, Rust’s main goal is no bugs in runtime, elixir puts more focus on graceful recoveries from crashes.
It depends on a use case, i tried Axum when i already read most of the rust book but it was still really hard for me to do even simple crud so i gave up as i figured out that maybe i don’t need that much safety and performance.
I picked elixir because it was easy to learn, offered nice syntax, was relatively safe and very performant in areas that matter for a web development project while being fault-tolerant.
Elixir does not do that. It will always tell you something even if you wait 30s for it. You’ll still get an error, or the OS process will be killed by f.ex. Linux’s out-of-memory reaper and then obviously nothing will get served as the server will not be there.
Your original question’s answer is: Rust can still crash and burn. You can write very defensively f.ex. never use .unwrap but maybe your colleague will have had a bad morning and will put it in. Then when you hit that place in the code and there’s no (or errored) value the entire OS process aborts.
Elixir does not do that – one error never brings down the entire OS process.
To expand a little on Rust: both Axum and ActixWeb are amazing frameworks and I am certain you’ll never have an OS process crash because of them. Still, you the developer can write a non-crash-proof code inside your responders / handlers and then all bets are off.
In Elixir you can straight up try writing this inside your controller function(s):
something = nil
IO.inspect(something.inserted_at)
Which is an error, but nothing will stop, just one request will fail and everything else goes as normal with zero capacity affected.
In Rust, if you write this:
value = None;
stuff = value.unwrap();
Then your entire program might get aborted and die on the spot. Though final disclaimer: I haven’t looked at Axum and ActixWeb in at least a year so maybe double-check; maybe they have added protections against defective code like this since the last time I checked them out.
A quick glance shows that there are some plugins to catch panics, but I feel compelled to add that “added protections” of this kind give people a false sense of security.
Yes, they let you ignore the error that just happened, but in doing so you also ignore any side-effects that occurred up to the point of the crash, potentially leaving your program in an inconsistent state (from the program’s point of view, not necessarily the language’s). Half the selling point of Elixir/Erlang, IMO, is that it gives you decent tools to handle this.
Yes. Rust has no runtime to speak of (tokio is kind of sort of a runtime but it’s not nearly rich on features as the BEAM VM) so basically if you don’t code defensively all bets are off.
Though in the spirit of a balanced discussion, Rust’s ecosystem has world-class tooling to help you scan for e.g. unsafe (via cargo-geiger) and you can also use a variety of linters or even roll your own (through ast-grep, gritql or just plain old grep or rg) and just disallow stuff like .unwrap at all. So a diligent Rust programmer – and I’ve worked with 15+ of them – will still have programs that don’t crash ever. But you’re right: that’s because the programmers are diligent and is not owed to the runtime. With Erlang / Elixir, the runtime is always there to catch us when we fall.
This is not a thing that is possible once external things that can misbehave are added to the equation. The simplest example would your controller doing a http request to another service. You can have a lot of things that can go wrong:
The service is down;
The service decided that they want to make a breaking change to their contract;
The network request failed for a reason or another;
You are hitting a timeout.
Unless you have covered all these edge-cases and more, you are never guaranteed that this thing will not blow up in your face, no matter how many compile-time guarantees you have. I think this is the exact point where defensive code becomes a liability and the perfect place where erlang VM excels, the system continues to work and you can decide later if you want to handle specific edge cases or some partial errors are acceptable.
That’s true, but on the other hand, even if we’re 100% certain that we cannot panic, a program that is stuck in an infinite loop (or simply stuck “long enough”) makes the program unable to service requests too. In Rust you’re hosed, in Elixir/Erlang you just get degraded performance, and can kill the offending process without making things inconsistent.
Not sure we’re talking about the same thing – I’ve coded a fair amount of Golang and Rust services that never had their OS process abort due to these. And it’s not strictly defensive programming btw; it’s utilizing one of the biggest advantages of Rust over many other languages, namely exhaustive pattern matching enforced at compile-time – something that Erlang / Elixir don’t have. In Rust you cannot ignore the error value like you can in Erlang / Elixir – this guarantees no blow-ups.
The code gets verbose fast but I still vouch for Rust. I have created 3 separate API apps that also call other 3rd party APIs and after we fixed an initial flurry of unexpected cases (something that took one week) then the services kept going for a full year, only being stopped for upgrades (which are of course done with blue-green deployments so no actual downtime).
You won’t catch me arguing with that, I preferred Elixir over Golang / Rust as you know. My point was that Rust in particular offers you “compensation” for the lack of a runtime like the BEAM – namely by forcing you to handle every edge case upfront.
People argue this is not interesting or productive but I’ve been on both sides and it’s honestly just a matter of which tradeoff do you prefer.
Yes, and that’s one of the chief reasons why I preferred to make a career with Elixir and not with Rust. You have to be 10x as careful when coding in Rust. I still love it, mind you, but for web / API work Elixir and even Golang are definitely more productive in the initial stages than Rust. Rust can kill your desire to program and make you a potato farmer if you don’t buy its diligence stance.
The funny thing is that I do buy its stance on diligence and then some, I actually like the language and I’m not here to dunk on it – I just wanted to provide some perspective that is often lost when thinking about “crashes” and how to deal with them.
I agree that the distinction and the tradeoffs bear repeating often – thanks for doing it! People easily fall into the “language X is better than Y” trap which, while often true for many languages, absolutely does not apply for the 10-15 top dogs where the game is “pick your poison because they are all good”. To me Elixir and Rust are one of those top dogs.
Read big files into memory and don’t let the variables keeping them go out of scope – f.ex. spawn a process (not OS process but an Erlang process; very different things), have it read a big file and store it in a variable, and then just have it sleep for weeks.
Phoenix itself is extremely well written and it will not make your server process crash.
Isn’t it true that what we’re really talking about two tools that are really aimed at different problems?
When I look at Rust, I see something that gets me in close contact with the running system with few abstractions: yes there are rules enforced by the borrow checker, rules about mutable state, and lifetimes, etc. But all of that is to protect me from the low level access I have (and all of which I can bypass with unsafe if I need to). Rust is a systems language.
When working with the BEAM VM, aren’t I really working at a different level of abstraction? Moreso than by using this or that library? A lot of that lower level stuff is dealt with by the BEAM and as an Elixir developer I’m largely protected from those details.
At least that’s my hand-wavy understanding.
I think this is why the earlier “Apples and Oranges” comment is really justified. The trade-offs are different, the guarantees are different, and all of those choices where made in efforts to satisfy different needs. Sure, there’s overlap in applicability, but I’d personally take the larger picture and consider what my application needs the most: many web apps don’t benefit from systems level programming… in which case allowing the good people building the BEAM handle those complexities for me is a win (especially considering they’ll likely deal with that level of complexity better than I would as a business systems programmer). However, at a certain level of performance requirement or computational complexity that situation could change (and even then I might write rust on the hot path and allow the BEAM to orchestrate the whole thing). Trying to straight up compare them without a better sense of these larger goals seems misguided.
I completely agree, different tools for different use-cases.
I think people are getting confused these days because every popular language tries to be “universal”, plus the mix of developers that follow this hype without being able to weight upsides and downsides, or making correct choices for the right technology.
I worked at a small company where they were debating between using go and rust for their services, when in reality the critical thing they needed was reliable uptime. They went ultimately with go (luckily that most probably saved their skin) and made an abomination of infrastructure to assure that reliability, when in reality they could have used elixir/erlang, work less on implementing features from scratch and sleep better at night knowing that everything will work even with errors in the system.