
Rust is known for having a steep learning curve, but in this post we will be looking at another curve, the one showing developer productivity in relation to the project size.
Last year I ported a virtio-host network driver written in Rust, entirely changing its backend, interrupt mechanism and converting it from a library to a standalone process. This is a complex program that handles low-level memory mappings, VM interrupts, network sockets, multithreading, etc.
What’s remarkable is that (a) I have very little Rust experience overall (mostly a Python programmer), (b) very little virtio experience, and (c) essentially no experience working with any of the libraries involved. Yet, I pulled off the refactor inside of a week, because by the time the project actually compiled, it worked perfectly (with one minor Drop-related bug that was easily found and fixed).
This was aided by libraries that went out of their way to make sure you couldn’t hold them wrong, and it shows.
I've been writing Rust code for a while and generally if it compiles, it works. There are occasional deadlocks and higher-level ordering issues from time-to-time, but modulo bugs, the compiler succeeding generally means it'll run a decent amount of your project.
Though my code complexity is far FAR from what you've been writing it has been a similar experience for me. There are a few footguns and a bug in chrono I still have to find the energy to report or fix; which has been causing a bi-yearly issue, but apart from that happy lil programmer.
I would be curious to know if you've looked at `jiff`. Does it resolve your bug/footgun issues with chrono?
It's a lot like Haskell in that respect. Once you get something to compile, it tends to work more often than not.
Reviews like yours are increasing my interested towards Rust. Apparently the tools and ecosystem are great, build on solid concepts and foundation.
Rust has 3 major issues:
- compile times
- compile times
- long compile times
It isn't that big of a deal in small projects, but if you pull dependencies or have a lot of code you need to think about compilation units upfront.
How is Rust in terms of incremental compile time? Is incrementally recompiling on file in a large project quick? Hopefully link times aren't that bad.
One thing I like about the JVM is hot code reloading. Most of the time, changes inside a method/function takes effect immediately with hot code reloading.
Link times are the worst part but solveable with mold[1]/sold. Incremental compilations are usually an order of magnitude (or even two) faster than clean compiles but tbh that can still feel slow. Helped by using something like sccache[2] or even cranelift[3] when debugging. Still not as fast as having a hot-reloadable language but it gets you to a relatively pleasant speed still IME
[1] https://github.com/rui314/mold [2] https://github.com/mozilla/sccache [3] https://github.com/rust-lang/rustc_codegen_cranelift
I've never been successful in getting sccache to really speed up projects, but then again only release builds have _really_ been impossible for me, and that's only when I was working on Deno which was absolutely massive.
sccache sped up my clean compiles by 2x on some projects, but it's a very YMMV solution most of the time
We had issues with long compile times at deno. Release builds were brutal, debug builds were OK as long as they were incremental. It was likely one of the largest open-source rust applications, but we were still quite productive.
Most likely you'll have years before it's an issue and there are mitigations.
I think Rust is awesome and I agree with that part of the article.
What I disagree with is that it's the fault of Typescript that the href assignment bug is not caught. I don't think that has anything to do with Typescript. The bug is that it's counter-intuitive that setting href defers the location switch until later. You could imagine the same bug in Rust if Rust had a `set_href` function that also deferred the work:
set_href('/foo');
if (some_condition) {
set_href('/bar');
}
Of course, Rust would never do this, because this is poor library design: it doesn't make sense to take action in a setter, and it doesn't make sense that assigning to href doesn't immediately navigate you to the next page. Of course, Rust would never have such a dumb library design. Perhaps I'm splitting hairs, but that's not Rust vs TypeScript - it's Rust's standard library vs the Web Platform API. To which I would totally agree that Rust would never do something so silly.Objection your honor ;)
A 'setter' should never ever cause an action to be triggered, and especially not immediately inside the setter.
At the least change the naming, like `navigate_to(href)`.
But in the browser environment it's also perfectly clear why it is not happening immediately, your entire JS code is essentially just a callback which serves the browser event loop and tells it what to do next. A function which never returns to the caller doesn't fit into the overall picture.
That’s a good point. I actually modified my comment because I assumed everyone would take for granted that no work should be done in a setter :)
> A function which never returns to the caller doesn't fit into the overall picture.
Hmm, not sure about this. On the node side, you can process.exit() out of a callback. If setting href worked like that, I think it would be less confusing.
> If setting href worked like that, I think it would be less confusing.
How do you imagine this would interact with try-finally being used to clean up resources, release locks, close files, and so forth?
try-finally is leaky in JavaScript, in any case. If your `try` block contains an `await` point, its finaliser may never run. The browser also has the right to stop running your tab’s process partway through a JavaScript callback without running any finalisers (for example, because the computer running the browser has been struck by lightning).
For this reason, try-finally is at best a tool for enforcing local invariants in your code. When a function like process.exit() completely retires the current JavaScript environment, there’s no harm in skipping `finally` blocks.
Thanks! I should have clarified a bit better that example.
The point I was trying to make is that Rust's ownership model would allow you to design an api where calling `window.set_href('/foo')` would take ownership of `window`. So you would not be able to call it twice. This possibility doesn't exist at all in TypeScript, because it doesn't track lifetimes.
Of course, TypeScript can't do anything here either way. Even if it had knowledge of lifetimes, the JavaScript API already existed before and it would not be possible to introduce an ownership model on top of it, because there are just too many global variables and APIs.
I wanted more to demonstrate how Rust's whole set of features neatly fits together and that it would be hard to get the same guarantees with "just types".
I'm not as familiar with Rust, but isn't there still a gap? For instance, if we modified window.set_href to have move semantics, wouldn't this still work (i.e. not produce an error)?
let win = window.set_href("/foo")
win.set_href("/bar")
You might say "why would you ever do that" but my point is that if it's really the lack of move semantics that cause this problem (not the deferred update), then you should never be able to cause an issue if you get the types correct. And if you do have deferred updates, maybe you do want to do something after set_href, like send analytics in a finally() block.In fact, Typescript does have a way to solve this problem - just make `setHref` return never[1]! Then subsequent calls to `setHref`, or in fact anything else at all, will be an error. If I understand correctly, this is similar to how `!` works in Rust.
So maybe TS is not so bad after all :)
[1]: https://www.typescriptlang.org/play/?ssl=9&ssc=1&pln=9&pc=2#...
Your Rust example would not work, because `window.set_href("/foo")` would not return anything (it would return the unit type "()" aka void). And you can't call `set_href()` again on "()". This is a common pattern in Rust, allow certain functions to only be called once on specific objects.
I really like your TypeScript solution! This actually perfectly solves the issue. I just wish that this was the ONLY way to actually do it, so I would not have experience the issue in the first place.
Thanks for the explanation! That's a very nice pattern.
> I just wish that this was the ONLY way to actually do it
I completely agree.
Just to add what the other comment said, take a look and run these two examples to see the errors you could get:
https://play.rust-lang.org/?version=stable&mode=debug&editio...
https://play.rust-lang.org/?version=stable&mode=debug&editio...
The Rust example was interesting but the Typescript example doesn't show if TS would or would not be good for a big project.
I'm scared of Ruby because I catch bugs at runtime all the time, but here's the thing: it ends up working before a commit and it was easy enough to get there and it's satisfying to read and edit the code. Now wether I can keep going like this if the project become bigger is the question.
The location.href issue is really a javascript problem that has been inherited by TS. Because JS allows to modify attributes, the browser kind of has to take the change into account. But it's not like Ruby's exit keyword. The page is still there until the next page loads and this makes total sense once you know it.
Technically Rust could hint at the semantics, based on whether `set_href` returned `()` or `!`. However the “erroneous” usage would not be surfaced in the case of a conditional redirect indeed (for a non-conditional one you may notice that the following code is not dead).
That is not a repo of the code in the article. The article's code is effectively
set_href('/foo');
let future = doSomethingElse()
block_on(future)
if (some_condition) {
set_href('/bar');
}
This code makes the bug clearer. doSomethingElse is effectively allowing the page to exit. this would be no different in many apps, even in rust.The browser does not start a process when you set `window.location.href`. It starts a process after your code exits and lets the event loop run other tasks. The `await` in the example code is what allow other tasks to run, including the task to load a new page, (or quit an app, etc..) That task that was added when you set `window.location.href`
If that's not clear
// task 1
window.location.href = '/foo' // task2 (queues task2 to load the page)
let content = await response.json(); // adds task3 to load json
// which will add task4
// to continue when finished
// task4
if (content.onboardingDone) {
window.location.href = "/dashboard";
} else {
window.location.href = "/onboarding";
}
task2 runs after task1. task1 exits at the `await`. task2, clears out all the tasks. task3 and task4 never run.No, I think you misunderstand how it works. The problem is that task 4, as you call it, runs after the navigation triggered by the redirect value.
The the author expects the side-effect -- navigation to a new page -- of the window.location.href setter to abort the code running below it. This obviously won't happen because there is no return in the first if-statement.
There is a return, it's disguised as "await"
*simplified*, the symantics of "await" are just syntactic sugar
const value = await someFunction()
console.log(value);
is syntactic sugar for return someFunction().then(function(value) {
// this gets executed after the return IF
// something else didn't remove all events like
// loading a new page
console.log(value);
});Yeah, the problem is that some old web APIs have been clearly hacked up haphazardly in the '90s, probably in a hurry, and now we have to live with the consequences of that. This is not unique with the Web though, it's basically the same with the entirety of the WinAPI and most libc functions in my experience
So, your argument is that Rust is better because better programers use Rust.
I specifically mean this part: "Rust would never have such a dumb library design".
One could then also say that Rust programmers would never make such a cyclical argument.
If you want to be more charitable, you could say "Rust library design is superior to the Web API library design", and I'd say you were right - particularly for crufty stuff like .href which was designed decades ago.
Code written below your line gets executed if you don't return early. More breaking news at 8.
Seriously, why would you think that assigning a value would stop your script from executing? Maybe the Typescript example is missing some context, but it seems like such a weird case to present as a "data race".
Assigning to `window.location.href` has a side effects. The side effect is that your browser will navigate to wherever you assigned, as if you had clicked a link. This is already a surprising behaviour, but given that this assignment is effectively loading a new page in-place, kind of like how `execve` does for a process, I can totally see how someone would think that JS execution would stop immediately after a link is clicked.
It's obviously not a good idea to rely on such assumptions when programming, and when you find yourself having such a hunch, you should generally stop and verify what the specification actually says. But in this case, the behaviour is weird, and all bets are off. I am not at all surprised that someone would fall for this.
Part of the problem is that we unknowingly make a million little assumptions every day in the course of software development. Many of them are reasonable, some of them are technically unreasonable but fine in practice, and some of them are disasters waiting to happen. And it's genuinely hard to not only know which are which, but to notice even a fraction of them in the first place.
I'm sure I knew the href thing at one point. It's probably even in the documentation. But the API itself leaves a giant hole for this kind of misunderstanding, and it's almost certainly a mistake that a huge number of people have made. The more pieces of documentation we need to keep in our heads in order to avoid daily mistakes, the exponentially more likely it is we're going to make them anyway.
Good software engineering is, IMHO, about making things hard to hold the wrong way. Strong types, pure functions without side effects (when possible), immutable-by-default semantics, and other such practices can go a long way towards forming the basis of software that is hard to misuse.
This is actually mostly related to a language's expressivity, which can simultaneously be used for good and for obscure stuff. (Also, JS having a rough evolution from a badly designed scripting language with hacky injection points to the browser, to being an industrial language at the core of the modern web, with strong backwards compatibility)
This can be made into an extreme (e.g. C/Zig tries to make every line understandable locally - on the other extreme we have overloading any symbols, see Haskell/Scala).
Honestly, the href thing feels like a totally reasonable assumption to me. I think the API design is unfortunate, but given that the API is designed as it is, it stands to reason that the script would also halt execution upon hitting that line.
For me, that's exactly the kind of thing that I tend to be paranoid about and handle defensively by default. I couldn't have confidently told you before today what the precise behavior of setting location.href was without looking it up, but I can see that code I wrote years ago handled it correctly regardless, because it cost me nothing at the time to proactively throw in a return statement.
As in this example, defensiveness can often prevent frustrating heisenbugs. (Not just from false assumptions, but also due to correct assumptions that are later invalidated by third-party changes.) Even when technically unnecessary, it can still be a valid stylistic choice that improves readability by reducing ambiguity.
> because it cost me nothing at the time to proactively throw in a return statement
This is how I’ve generally always handled redirects, be it server or client - if I’m redirecting the user somewhere else, my expectation is that nothing else on this page or in this script _should_ run. Will it? Maybe, JavaScript is weird. To avoid the possibility, I’m going to return instead of just hoping that my expectations are met.
> when you find yourself having such a hunch, you should generally stop and verify what the specification actually says
It greatly heartens me that we've made it to the point where someone writing Javascript for the browser is recommended to consult a spec instead of a matrix of browsers and browser versions.
However, that said, why would a person embark on research instead of making a simple change to the code so that it relies on fewer assumptions, and so that it's readable and understandable by other programmers on their team who don't know the spec by heart?
I'm pretty sure it did used to work the other way. Even if it didn't something changed recently so that the "happen later" behavior was significantly more likely to be encountered in common browsers.
Is this a JavaScript wart or a browser wart though? JavaScript is communicating to the browser via an API and rust would need to do the same.
exit(), execve(), and friends do immediately stop execution—I could understand why you'd think a redirect would as well.
Exactly. Given that JavaScript runs in the context of a page, redirecting off of the page seems like it should act like a "noreturn" function...but it doesn't. That seems like a very easy mistake to make.
Until they don't. A common issue is not checking if the execve() actually worked and thinking nothing after the execve() will execute, which is an assumption that it not always true.
See, that's what they meant about making assumptions upthread:
The redirect is an assignment. In no language has a variable assignment ever stopped execution.
$ python3
Python 3.13.7 (main, Aug 20 2025, 22:17:40) [GCC 14.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class MagicRedirect:
... def __setattr__(self, name, value):
... if name == "href":
... print(f"Redirecting to {value}")
... exit()
...
>>> location = MagicRedirect()
>>> location.href = "https://example.org/"
Redirecting to https://example.org/
$You're overloading a setter here. It's cute, I did it in JS as well, but I don't really think it's a counterexample. It would be odd to consider this the norm (per the thought process of the original blog post).
This is not some weird thing. Here is a run of the mill example where python can have properties settting do anything at all. And it's designed like that.
import sys
class Foo:
@property
def bar(self):
return 10
@bar.setter
def bar(self, value):
print("bye")
sys.exit()
foo = Foo()
foo.bar = 10
Or in C# if you disqualify dynamic languages: using System;
class Foo
{
public int Bar
{
get { return 10; }
set
{
Console.WriteLine("bye.");
Environment.Exit(0);
}
}
}
class Program
{
static void Main()
{
Foo obj = new Foo();
obj.Bar = 10;
}
}
This is not some esoteric thing in a lot of programming languages.You're also overriding a setter. Maybe I'm going against the grain here, but it's absolutely esoteric. The assignment operator is not supposed to have side-effects, and maybe this is the logician in me, but the implication that we should be aware that weird stuff might be happening when we do `x = 5` is fundamentally bonkers.
You started with "In no language has a variable assignment ever stopped execution", and now you're saying "The assignment operator is not supposed to have side-effects". location.href is a counterexample, and there are many counterexamples throughout various tools and languages and libraries. Deciding how you think things should work does not affect how things do work, and it's important to understand the latter. (I do agree it's bad practice, but it happens and people do not always fully control the environments they must work with.)
And given that location.href does have a side effect, it's not unreasonable for someone to have assumed that that side effect was immediate rather than asynchronous.
That said, if you don't like working with such languages, that's all the more reason to select languages where that doesn't happen, which comes back to the point made in the article.
> You started with "In no language has a variable assignment ever stopped execution",
The irony is that I'm still technically correct, as literally every example (from C++, to C#, to Python, to JS) have been object property assignments abusing getters and setters—decidedly not variable assignments (except for the UB example).
The entire discussion is about a property assignment. Which in colloquial usage is also called variable assignment. Which is obvious since nobody corrected you on that. You now trying to do a switcheroo is honestly ridiculous.
The entire discussion is about “=“ doing weird stuff, which in 99.9% of cases it does not do. And my point was that no language, without doing weird stuff (like overloading), does not let “=“ do weird stuff (and thus is pure). The counterarguments all involve nonstandard contracts. Therefore, thinking that using “=“ will have some magical side-effect is absolutely never expected by default.
> The counterarguments all involve nonstandard contracts. Therefore, thinking that using “=“ will have some magical side-effect is absolutely never expected by default.
That sounds like a recipe for having problems every time you encounter a nonstandard contract. Are you actually saying you willfully decide never to account for the possibility, or are you conflating "ought not to be" with "isn't"?
If I'm programming in a language that has the possibility of properties, it's absolutely a potential expectation at any time. Which is one reason I don't enjoy programming in such languages as much.
To give a comparable example: if I'm coding in C, "this function might actually be a macro" is always a possibility to be on guard against, if you do anything that could care about the difference (e.g. passing the function's name as a function pointer).
So, with anything that isn't a primitive type (e.g. int, bool, etc), there's a chance that assignment is going to require memory allocation or something similar. If that's the case then there's a chance of bad things happening (e.g. a out of memory error and the program being killed).
More commonly, if you look at things like c++'s unique_ptr, assignment will do a lot of things in the background in order to keep the unique_ptr properties consistent. Rust and other languages probably do similar things with certain types due to semantic guarantees.
In languages like JavaScript (or Python etc) you can get memory allocation etc even when 'primitives' like int and bool are involved.
Haskell is the same, but for different reasons.
Technically no. Producing side effects from a setter is not unexpected, even if it often the best idea to have a setter have a lot of unexpected side effects. However producing side effects from getters is definitely unexpected and should not be done. Interestingly it's one of the areas where rust is really useful, it forces you express your intent in terms of mutability and is able to enforce these expectations we have.
Overloading assignment operators to maintain an invariant is one thing, but this particular case of it running off and effectively doing ionis weird to me coming from an embedded c++ background. I don't like operator overloading and think it should be avoided, just to make my bias obvious. I don't code c++ anymore either, rust and no looking back for a few years now.
But it doesn't run off and do I/O ... that's the bug! The OP assumed that setting the variable causes the new page to be loaded but it doesn't--it just says what page should be loaded. The page doesn't get loaded until the app goes idle. So this whole discussion about setters and side effects is completely off kilter.
> Interestingly it's one of the areas where rust is really useful, it forces you express your intent in terms of mutability and is able to enforce these expectations we have.
Though Rust only cares about mutability, it doesn't track whether you are going to launch the nukes or format the hard disk.
True. But I would not expect any programming language to do that.
Rust provides safeguards and helps you to enforce mutability and ownership at the language level, but how you leverage those safeguards is still up to you.
If you really want it you can still get Rust to mutate stuff when you call a non mutable function after all. Like you could kill someone with a paper straw
> True. But I would not expect any programming language to do that.
Haskell (and its more research-y brethren) do exactly this. You mark your functions with IO to do IO, or nothing for a pure function.
Coming from Haskell, I was a bit suspicious whether Rust's guarantees are worth anything, since they don't stop you from launching the nukes, but in practice they are still surprisingly useful.
Btw, I think D has an option to mark your functions as 'pure'. Pure functions are allowed internal mutation, but not side effects. This is much more useful than C++'s const. (You can tell that D, just like Rust, was designed by people who set out to avoid and improve on C++'s mistakes.)
Assignment is by definition a side effect.
This whole discussion is completely off kilter by all parties because setting the variable doesn't terminate the script--that's the bug; it simply sets the variable (that is, it sets a property in a globally accessible structure). Rather, some time later the new page is loaded from the variable that was set.
Aside from that, your comments are riddled with goalpost moving and other unpleasant fallacies and logic errors.
FWIW I grew up in the days (well, actually I was already an adult who had been programming for a decade) when storing values in the I/O page of PDP-11 memory directly changed the hardware devices that mapped their operation registers to those memory addresses. That was the main reason for the C `volatile` keyword.
> The assignment operator is not supposed to have side-effects,
Memory mapped I/O disagrees with this. Writing a value can trigger all sorts of things.
assignments are side effects, even more so when they are done through a setter on an object / class instance
But window.location.href is already an overloaded setter. It schedules a page navigation.
> The redirect is an assignment. In no language has a variable assignment ever stopped execution.
Many languages support property assignment semantics which are defined in terms of a method invocation. In these languages, the method invoked can stop program execution if the runtime environment allows it to do so.
For example, source which is defined thusly:
foo.bar = someValue
Is evaluated as the equivalent of: foo.setBar (someValue)Try this in C:
*(int*)0 = 0;
Modern C compilers could require you to complicate this enough to confuse them, because their approach to UB is weird, if they saw an UB they could do anything. But in olden days such an assignment led consistently to SIGSEGV and a program termination.
Unless you were on systems that mapped address 0 to a writable but always zero value so they could do load and store speculation without worry.
IBM did this for a long time
My favourite were older embedded systems where 0 was an address you actually do interact with. So for some portion of the code you WANT null pointer access. I can't remember the details but I do remember jumping to null to reset the system being pretty common.
Probably the system interrupt table. Index 0 might reference the handler for the non-maskable interrupt NMI, often the same as a power-on reset.
I recall that on DOS, Borland Turbo C would detect writes to address 0 and print a message during normal program exit.
RANDOMIZE USR 0
In Wasm you can read/write whatever to address zero of linear memory.
It's still UB as far as clang is concerned so you C code can do whatever. But it won't “crash” on the spot.
You could overload operator=() in C++ with a call to exit(), which fulfills "variable assignment that halts the program".
And for a Rust contrived example, making += terminate execution,
use std::ops::AddAssign;
use std::process;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl AddAssign for Point {
fn add_assign(&mut self, other: Self) {
*self = Self {
x: self.x + other.x,
y: self.y + other.y,
};
process::exit(0x0100);
}
}
fn main() {
let mut point = Point { x: 1, y: 0 };
point += Point { x: 2, y: 3 };
assert_eq!(point, Point { x: 3, y: 3 });
}I was ignoring these kinds of fancy overload cases, but even in JS you can mess with setters to get some unexpected behavior (code below).
idk if I'd consider overloading the assignment operator to call a function, then using it, actually an assignment in truth.
Well, when you read the source of the caller, it looks exactly like a normal assignment.
That doesn't seem that obvious to me. You could have a setter that just calls exit and terminates the whole program.
Yeah, this is actually a good point, could have a custom setter theoretically that simply looks like assignment, but does some fancy logic.
const location = {
set current(where) {
if (where == "boom") {
throw new Error("Uh oh"); // Control flow breaks here
}
}
};
location.current = "boom" // exits control flow, though it looks like assignment, JS is dumb lolIn Blink setHref is automatically bound to C++ code [1]. I think it's fair to say that anything goes.
[1]: https://source.chromium.org/chromium/chromium/src/+/main:thi...
It seems weird to shame someone for talking about their own experience?
Whether you think that or not is not the issue - the fix is very obvious once pointed out to you. The arguement the author is making is that a bug like that TS issue can be very difficult and time consuming to track down and is not picked up on by the compiler.
Whenever someone talks about a surprising paper cut like this you always see misguided "this is obvious" comments.
No shit. It's obvious because you literally just read a blog post explaining it. The point is if you sprinkle dozens of "obvious" things through a large enough code based, one of them is going to bite you sooner or later.
It's better if the language helps you avoid them.
> Seriously, why would you think that assigning a value would stop your script from executing?
This assignment has a significant side-effect of leaving the page, assuming this is immediate rather than a scheduled asynchronous action is not unfair (I’m pretty sure I assumed the same when I saw or did that).
This is more a control flow issue than a data race issue. I've seen this countless times. And it is often a sign that you don't spend too much time writing JavaScript/Typescript. You get shot in the foot by this very often. And some linters will catch this - most do actyally
OP thought the redirect was synchronous, not that it would stop the script from executing
No, you're mistaken. Read the other comments under the parent. If it were synchronous then it would have stopped the script from executing, much the way POSIX exec() works. If the OP didn't think that the script would stop, then why would he let execution fall through to code that should not execute ... which he fixed by not letting it fall through?
I see, thanks