Skip to Content

Magical Software Sucks

Throw errors, not assumptions.

Published on

"It just works" crossed through with a red line, surrounded by random characters.

I hate magic in code. Magic is for end-users. To swipe your credit card and make a purchase without having to know, think, or care about what happens in the background. To turn on noise cancellation and instantly silence the outside world along with all the annoying people in it.

As a software developer, your job is to turn the lifeless metal bricks in people's pockets and backpacks into artificial brains that can think billions of times faster than yours. The bad part is that these artificial brains are only as smart as you are. If you put in shitty logic, you get a shitty result.

To avoid shitty logic, you need to know what's happening in your code. If there's "magic" anywhere in the mix, you have a black box with no obvious cause and effect. This means that what you think is happening and what actually happens start to diverge. Naturally, you'd think that what happens is what you want to happen. But you can never be sure. It's magic.

Take PHP's magic methods(opens in new tab) as an example:

class Foo
{
	public function myCustomMethod()
	{
		echo 'Hello, dear sir!';
	}

	public function __call($name, $args)
	{
		echo 'Doing something stupid and obscure…';
	}
}

Here I have a class with a regular method and a magic method that acts as a fallback handler function which gets called whenever you attempt to invoke an undefined method. This may sound useful on paper but it also makes it possible to do radically different things without knowing, due to a simple typo:

$foo = new Foo();
$foo->myCustomMethod(); // Hello, dear sir!
$foo->myCutsomMethod(); // Doing something stupid and obscure…
// typo ──┴┘

This is especially a problem when you're working with third-party code that follows such patterns. Gee, thanks a lot, if it weren't for your magic, I wouldn't have spent 5 hours trying to figure out what the hell is going wrong. Rather than assuming I wanted to do something completely unrelated, just throw me a goddamn error, so I can fix my typo.


If you're familiar with front-end development, another example is the succinct state management in Svelte(opens in new tab), compared to the more verbose state management in React(opens in new tab). As the meme(opens in new tab) goes…

Khaby Lame meme making fun of the verbose nature of React, compared to Svelte.

Sure, Svelte's approach is way more easier to understand initially, but it's not much different. It's just that the complexity is hidden away from you and deemed "magic."

As with everything magical, there are the pitfalls, which the Svelte team has acknowledged with their new runes feature(opens in new tab):

The reality is that as applications grow in complexity, figuring out which values are reactive and which aren't can get tricky. And the heuristic only works for let declarations at the top level of a component, which can cause confusion. Having code behave one way inside .svelte files and another inside .js can make it hard to refactor code, […]

To be clear, this is not me trying to hate on Svelte. I'm just saying that it's all a trade-off. You either:

  • spend time initially to learn and then write code that is more or less boilerplate, or

  • spend time later to debug internal behind-the-scenes framework logic you're unaware of.

I personally prefer to write a few extra lines, compared to debugging, reading docs, and digging into source code. Even if you can make the magic work for you, you can't be sure for how long it's going to last, so you still have to think about it. And as I suggested in the beginning with my credit card and noise cancellation examples, the whole point of things that work like magic is to not think about them.

Rather than trying to sweep complexity under the rug, I think it's better to just build strong boundaries around it, help developers understand it, give them control over it, and throw errors when the aforementioned boundaries are violated. Perhaps Rust's excellent error handling(opens in new tab) is one of the reasons it's the most admired programming language in 2023(opens in new tab).

There are many more examples where languages, libraries, and frameworks attempt to help us by introducing clever APIs that make assumptions… and cause immense headaches as a result. The problem with the "it just works" mentality is that there will come a time when "it" will inevitably cease to "just work."

Conclusion

Software is built by humans and humans make mistakes. Therefore, software should be everything but magic. It shouldn't be overly concise and clever — it should be explicit and predictable. It shouldn't make assumptions — it should throw errors.

Magic exists only in fantasy books. Behind every magical user experience there is plenty of not-so-magical machinery. As developers, we are the ones who build that machinery. We don't use the magic. We make the magic. If we try to combine both, we may get unpredictable and unintended results, so the magic in our code can ruin the magic for the end-user. Make the final product magical, not the software that runs it.