So, hear me out. At this point, “I understand monads” is basically a meme. Nobody understands monads. And, once you understand them, “you lose the ability to explain them.”1 This means: no promises for this article. There are only two options: Either I actually understand monads, and you won’t understand what I write here, or I didn’t actually understand monads, and so you won’t be any the wiser.
This article is for you if you either (a) share the same, subconscious urge to understand this programming concept; or (b) if you want to simply follow me down yet another rabbit hole. For me personally, writing this article feels relieving, because my brain can now follow more relevant political and sociological puzzles for the final weeks of my PhD studies!2
Why Is It So Hard To Understand Monads?
First, why is it so hard to understand monads? I have been trying to understand monads for the past five years, on and off. I wasn’t consumed by the thought that I didn’t yet understand them, but once or twice a year, I got this urge and wanted to know more about them. But I never really got them. And I grew frustrated. Because over time, I understood transformers (both encoder and decoder layers), and I even understood word2vec enough to build a functional prototype. The more I understood complex concepts of programming, statistics, and mathematics, the more it bugged me that I didn’t understand “that one concept.” I am always keen on improving my knowledge of programming, which led me from class-based OOP (Object-Oriented Programming) to functional programming, and I felt like monads would be the next step. However, they always eluded me.
I believe there are several reasons for why this is. The first is, more tongue-in-cheek, that, once you actually get them, you “lose the ability to explain them.” The second, I believe, is that we all come from different trajectories into programming, and thus need a different starting point to understand monads. A third is that monads basically break how we think about programming languages. And a fourth is that we really don’t need monads all that often.
The first reason is obviously not meant entirely serious, but there is a kernel of truth to it. To everyone who does not understand monads, including me, every explanation sounds like enchanting a mantra, or cursing your family. I mean, come on — “a monad is a monoid from the category of endofunctors.” Excuse me? I kept on reading new and new explainers, but all simply used the same magic incantations to “explain” monads. There was simply no breakthrough moment, because I just could not make the necessary connections between the words, their meanings, and, most importantly, the why of monads.
The thing is, math is a very efficient language. Math uses very short and concise words, but words that come with a barrage of meaning. It’s easy to say “Monads separate pure functions from impure functions, and they elevate values from the concrete context into the abstract monadic context until we execute the functor and retrieve the result.” But entire books could be written on that modest sentence to explain it. And these words are so rare that my spell checker has littered this paragraph with red squiggly lines.3
The second reason is something I just realized recently. All of us who didn’t enjoy a formal computer science education (a.k.a. most people who write code) usually started at some odd point in time. My first entry into programming was C++, Java, and then PHP. All of which are heavily class-based, that is, these are languages that encourage you to write object-oriented code. Later, I switched into more functional programming, when it actually started to make sense. Other people may start with Bash, Python, or simply R. A select few have started with Haskell and thus learned monads immediately. Nowadays, more and more people start by learning Rust and get exposed to monads from the start. All in all, this means that, regardless of where we come from, we need different analogies and mental models to build the necessary bridges between the origin of monads in math, and their implementation in software. For me, for example, I felt like the breakthrough came in a video that explained monads using Python (a language I know), and then made a connection to Rust’s Option type, which finally made it “click.”
The third reason speaks to the fact that monads are (to my very limited knowledge) the only concept that is still extremely close to its mathematical foundations and thus is more difficult to understand than, say, functional programming. And even though there are many bridges between math and programming, there is this fine distinction between pure math and the more pragmatic world of software engineering. Some concepts simply do not translate well from one world into the other. Also, the problem that monads solve is a very particular one that has been solved differently in nearly all existing programming languages, and so monads just don’t make a ton of sense in most of them. In other words, you literally have to break how you think in whichever programming language you know, in order to be able to understand monads.
Lastly, every explainer of monads that I have seen so far has used extremely simple examples to explain monads, but as I have realized, if you have a very simple program, monads actually overcomplicate it. And I am a person that always needs to know a reason for why we do certain things. If nobody gives me a reason, then why should I understand it? Monads simply do not make sense in simple programs, and the more data you have, the less it makes sense. In fact, I would argue that relatively shallow, pipeline-style data-analysis programs (that is, 90% of the programs I write in my research, because I work with data), would be hurt if we used monads in them.
Add to these reasons the fact that, by now, monads have become basically a meme, and you have a good sense for why monads are (a) so mysterious, and (b) so difficult to understand.
Side note: when I started the day (I originally wrote this article) I think I understood monads, I started with asking an LLM. I realized that, since explainers for monads are everywhere, every GPT model should have at least a few dozens of them in their training data. But after first querying Llama v3.2, then Mistral, and lastly ChatGPT, I realized that none would be able to explain monads to me. All of them, when asked for “why” we should use monads, replied the same: “It’s easier to reason about your program.” Yes, I’ve read that sentence a hundred times, but why?! This has cemented my suspicion that most monad-explainers basically just use the same words, which all don’t make sense to someone who doesn’t yet understand monads.
TL;DR: Monads are Essentially a Way of Error Handling
After that, the very abridged claim for what monads actually are (according to my understanding, which may be totally wrong) is quite simple: It’s a programming pattern that handles errors in a particular way. This is what caused the “Aha” moment in my understanding: As I was watching this guy explain monads using Python, he said the magic words: Monads are a way to handle errors that is different from how most programming languages handle errors. Aha! Suddenly, it all made sense.
First, the video explained why it is so hard to see a reason for them to exist: Because we don’t need them. Each language has some error handling. Most have no monads, but we survive. Second, Rust uses monads in its Option type, and since it enforces a very different type of error handling, it explains why writing Rust always felt off to me. Third, how they actually work is slightly odd, but it is possible to understand it if one tries to set aside the fancy math language for a second. Fourth, it explained why it was so difficult to grasp the functionality of monads: Because most explainers use Haskell to explain monads. But Haskell has a built-in syntax for monads, meaning that it literally hides away the actual functioning of monads. In other words: Every tutorial (except the one from Computerphile) that uses Haskell to explain monads explains how to write Haskell code, but not how monads work.
With that out of the way, let’s get into my own take on monads, in the hopes that I avoid using jargon to explain jargon to you. But again, I might not have understood monads, actually.
What are Monads?
Let us build up an understanding of Monads, trying to explain the crucial parts of the weird monadic language as we go.
Side Effects
At the foundation of an understanding for monads is the concept of “side effects.” A side effect, in plain terms, is anything where your code interacts with the external world. Here, “external world” is defined as everything outside your CPU. The idea is that your program “lives” in the CPU, and we assume the CPU not to die. When you write a function that adds up 2 and 2, and it does not take any input, it cannot, by definition, have any side effect, because your CPU will be able to calculate that. (We’re assuming that a computer “just works” and ignore weird stuff like cosmic rays here.)
A side effect, then, is anything unexpected that happens. This doesn’t happen when your computer calculates 2+2, but it can happen when you read from your disk. Because the file you wanted to read may not be there. Or, when you want to fetch a file from the internet, but the computer your code runs on has no internet. Finally, an important but overlooked “side effect” is printing stuff to a console. This can also go wrong, as it requires your computer to have a display, a cable to that display, etc. I hope you get the gist. Any interaction with any piece of hardware outside your CPU is a side effect.
Essentially, every program must have side effects, otherwise it is not going to do useful things for you. Any useful program will have at least two side effects: First, getting data into it, and second showing you the result. Both of these things happen outside the “pure” world of the CPU, and so — theoretically — something bad can happen. The aim of monads is, as texts frequently say, to “push side effects to the edges of your program.” I always thought this meant “run side effects either at the beginning or the end, but never in between.” Instead, I figured out, side effects can occur at any time. It is not when you run them, it is where you deal with them.
Error Handling: Monads Vs. The Rest
This was the core blocker for my understanding of monads. Most programming languages have a try-catch construction that allows you to safely run a piece of code that may throw, catch the error, and instead of outright crashing, try to recover. For example, take the following Node.js function that might read a plain text file from disk:
function readFile (path: string): string {
return fs.readFileSync(path, 'utf-8')
}
Looks good, right? Except when the file at path doesn’t exist, or your program lacks permission to read it. Then, the function readFileSync will throw an error. But the function I wrote is oblivious to it, it doesn’t catch it. So the error will be passed to the calling function, and so on. Now, in most programming languages, including every one I use, you would have to decide: What do I do in case of an error? Who should handle that? If you have a file that should extract, say, the mean of a column from a CSV, you might do the following:
function meanCSVColumn (path: string, col: number): number|undefined {
try {
const fileContents = fs.readFileSync(path, 'utf-8')
const csvData = parseCSVData(fileContents)
const col = csvData[col]
return mean(col)
} catch (err) {
return undefined
}
}
What we did here is very important: The function suddenly got an opinion, and it decided: If there is any error, I simply return undefined. Regardless of whether the file doesn’t exist, it’s not valid CSV data, or the column doesn’t exist, or it’s not a numeric column. We don’t care about it, we only want either the mean of that column, or undefined. This then means that whoever calls this function needs to handle the undefined result:
function selectFile (path: string): void {
const res = meanCSVColumn(path, 3)
if (res === undefined) {
showDialog('CSV seems to be wrong: Could not calculate mean of column 4.')
} else {
// ... use the result ...
}
}
This code is already somewhat “monadic.” Why? Because the function meanCSVColumn cannot fail. There will never be an error thrown. It fails not by throwing, but by returning undefined. The caller — here the function selectFile — checks whether the function fails, and if so, displays a proper error message to the user. I have chosen this name on purpose: selectFile is supposed to be called once the user has selected a file to open. In other words, the function runs by request of the user. The mean calculating function should not throw errors, it should just either return undefined, or said mean. It should not do error handling, because it doesn’t know what the caller’s intention was. Is the caller fine with the function failing? In that case, throwing an error would be bad. Is the caller not? Then let them decide to display an error to the user that makes sense in context.
Monads essentially just abstract all of this into a nice programming pattern. That’s all there is to it. Monads ensure that error handling never happens while some functions do something to your data, but it is always the caller’s responsibility. We could make the selectFile monadic, too. If we decided that we should not do any error handling here, we could simply make that function return undefined, too. Then it would be up to whichever function calls selectFile to check if everything works properly.
Monads literally are just neat types around these two states. A function that either returns a number (a.k.a.: a result) or undefined (a.k.a.: it failed), could also just either return Some(number), or None(). This is precisely what Rust’s Result and Option types do. They return some result, or nothing, and based on whatever a function gives back, you immediately know if it succeeded or not. In fact, this means that you won’t need null, undefined, void, or whatever anymore to indicate failure. In fact, a function could return undefined as a successful result!
But this also shows why monads often do not make sense: Programming languages that have a built-in way to represent failure, such as undefined or throwing errors naturally do not need monads. The function I have provided above uses Node.js’s built-in file reading function. As a monadic function, it could look like this:
function readFile (path: string): Result<string> {
return fs.readFileSync(path, 'utf-8')
}
It looks almost like the original function. But instead, now the function doesn’t throw an error, but it always returns a result container which may contain a string (if the read was successful), or an error. Now you see why monads can make “reason about code” simpler: They are explicit. When it comes to error throwing, you will always need tracebacks to find the source of the error, but with monads, it can be faster to follow the call-chain to find where the error originated. In addition, you can see why implementing monads should usually follow the language: If a function already returns a monad instead of throwing errors, it would be weird to take the function result and throw an error if the result is None. Instead, it is much simpler to just pass on that monad to the next function.
You can “force” monads into try-catch languages, however. As a final example, take a look at the following snippet that takes the readFileSync-function as it is, but turns it into a monad:
function readFile (path: string): Result<string> {
try {
return Ok(fs.readFileSync(path, 'utf-8'))
} catch (err) {
return Err(err)
}
}
Now, with the main purpose of monads at hand, let’s discuss all the other fuzzy terms that are frequently being thrown around when trying to explain monads.4
Pure vs. Impure Code
Monads are often explained in that they separate pure from impure code. But for that, we have to understand what this is. This is relatively straight forward now. “Pure” code is code that cannot have any side effects whatsoever. “Impure” code, on the other hand, can. Consider the following program:
function square (x: number): number {
return x ** 2
}
result = square(5)
This is a pure program. Which means: Under the assumption that your CPU doesn’t suddenly explode or that there is some cosmic ray flipping a bit in its L1 cache, it will always, until the end of time, be guaranteed to take the number five, and square it. So let’s print that out:
function square (x: number): number {
return x ** 2
}
result = square(5)
console.log(`Result: ${result}`)
Oh no! We just wrote impure code! The console.log function tries to print “result” to the screen. This requires there to be an stdout stream. And this could fail. Because the print statement interacts with the outside of the processor, here: all the hardware required to take data from the CPU (the result) and put it onto a computer display. This makes it impure.
Bottom line: There is no pure program. Every program must at some point interact with the outside world, which might fail, so it’s impure. There are always side effects. A perfectly pure program would just run, and not leave any trace of it. We would never know what it actually did. In other words: It’s useless to us.
This means that the distinction pure vs. impure makes more sense when talking about certain functions, or sections of your code. Some parts of your code may be pure, because they do not rely on anything external that could fail. (Again, the CPU strictly speaking is … external to the program, but we just assume it can never fail.) But your program in total is always impure, because at some point you inevitably need input or write text on the console.
However, the aim of a monad is not to separate pure from impure code. This is a big misunderstanding that I always had. The aim of functional programming is to separate pure from impure code.5 The aim of monads, however, is something else. Monads embrace impure code, and they don’t care whether some function is pure or impure. Instead, monads are a way to move error-bearing code to very particular parts of your application where you control what happens if something goes south.
This also finally explains why it is difficult to see the point of monads when all you get as examples are very simple ones like the one in the Computerphile video.
Functor
The next tricky term is “functor.” Many have tried to explain it to me, but once I learned that a functor is just a data structure that combines a value with a function, it made more sense. The following is a crude approximation of a functor in JavaScript:
function Functor (value, func) {
return { value, func }
}
As you can see, a functor is nothing but a data structure which includes both some data and a function that can operate on that value. But it doesn’t (yet). This will become important later, but for now: A Functor basically “stores” a function call and a value you can provide to that function for later calling.
Endofunctor
Now you can understand an endofunctor. The prefix “endo” (Greek, you know how math works) basically just means something akin to “within” or “contain.” An endofunctor is essentially just a functor that returns something that has the same type signature. So if you have a functor of type int, when you call it, it must also return a functor of type int. When it returns a functor of type string, it’s not an endofunctor anymore. This makes more sense with actual types. Say you have a functor that takes a number and a function that operates on the number:
function Functor (value: number, func: (val: number) => Functor) {
return { value, func }
}
This is not an endofunctor, because it does not return something of the same type yet. It takes a number, but returns a functor. To achieve that, you will need a functor that accepts itself as its value. However, the opposite also works, and you can just write a functor that accepts any as the value:
function Endofunctor (value: any, func: (val: any) => Endofunctor) {
return { value, func }
}
Now it doesn’t matter whether you call Endofunctor(5) or Endofunctor(Endofunctor(null)). However, if you noticed: We just got rid of the data types here. There’s nothing stopping you from providing whatever value to it. This is (a part of) the abstraction that people often talk about, and this is how Rust’s Result type works: It accepts any type of data. The only important part for the Result monad is that it can be either Ok or Err, a container for the value (Ok), or the error message (Err).
Abstraction
Monads are “abstract” in such a way that they don’t really care about what value they hold. Again, a monad is a way of error handling, and errors can appear in dozens of operations. Remember: most code is impure. This means that monads need to be able to ignore the values they receive. However, you will frequently find people tell you that monads have very specific function signatures: int --> int --> double. This is not essential for a monad because, again, it wants to abstract. Reintroducing some specific data types doesn’t help understand them.
This is best understood by finally introducing something that clicked for me, too, once I understood monads: Rust’s Option and Result types. In Rust, you typically have Options and Results. Specifically, you have a lot of functions that either return Some or None, or Ok or Err. Structurally, Options and Results are one and the same. The only difference is that Err contains an error description that can help you understand what went wrong, whereas Options only give you None with no further explanation.
If a function gave you a Some you know that there is a useful value in it which you can use. However, if that function returned a None, you know that something went wrong. What you actually put into that Some is completely up to you. You could use Some(5), or Some(“string”), or even Some(null). What is important is that the value is wrapped in a Some, and not in a None. You don’t really interact with Option directly, because that is just a type, which has two instantiations: Either a Some or a None. And whichever it is, you know whether the function call succeeded, or not.
If a program that accesses some file on disk and parses its contents returns a Some to you, then you know that (a) that file existed, (b) its contents were valid, and (c) you now have the data and can work with it. If the program returned you None you know that something has gone wrong: Either the file didn’t exist, or your program did not have read access, or the data couldn’t be parsed. This already gives you some glimpse into how monads perform error handling.
This is by the way also why many programming languages that support monads or are built on them make heavy use of match statements. In Rust, for example, you often match all available results from a function call, so that you explicate what the program should do if the call succeeds, versus if it does not. In Haskell, there’s something similar.6
Here’s a toy example from Rust demonstrating the use of the match operator:
let result = divide(a, b);
match result {
Some(value) => println!("The result is: {}", value),
None => println!("Cannot divide by zero"),
}
“Elevating” Values
Another term you will frequently stumble upon is that monads will “elevate,” or “lift,” values into some abstract realm, before moving them back down again. I always found this metaphor appalling, because it doesn’t seem to make sense. But essentially, what it means is to wrap a value in a functor. Essentially, at the start of running some monadic code, you will just have a plain value. Something boring and plain, such as a number, or a string, or a Boolean. Then you wrap it into such a functor thingy. By doing so, you “elevate” the value into the realm of monads. Once you unwrap this function (note the term “unwrap”), you essentially pry the value out of the functor, that is, access the value from this functor.
However, for now forget about this metaphor, because I personally find it very unhelpful. Instead, think rather about “wrapping” a value into a data structure, because that’s what happens in practice.
Monoid
A monoid is some math concept that I don’t completely understand, but I gathered the following: it is something that defines some operations and its result. The Option monad in Rust, for example, comes in the flavors Some and None, and you can chain up computations you do on whichever value is wrapped in Some, including some operations. Monoids also need to be commutative (so the order of computations should not matter). And so on. As you can see: What you actually call the things is pure semantics at this point. The important part is this “chains computations on some piece of data.”
“Binding”
Another term I frequently encountered is to “bind” something. This is, if I am not mistaken, just a fancy term for chaining up functions together. In Rust contexts, I have seen and_then, and some LLM has spat out of. Remember, monads are a variant of functional programming, where the idea is that you have some value, and you want to massage it a bit, until you get the result. The “binding” essentially just means to add one additional step. For example, you may want to take a number and multiply it by three, and then divide by two. “Binding” then just means that you take the value, wrap it into a monadic type, queue up a function to multiply it by three and divide by two, and then running the functions.
Deferred Computing
One term that I don’t read often but that I find fundamental to understanding monads is that it also involves deferred computing, meaning: A monad first involves chaining data transformations you want, and then, once you’re done with it, executing all of these functions and seeing what pops out at the end. This is counterintuitive with most of the examples people use to explain monads, because in those toy examples, execution is practically instant – both theoretically, because there is little chaining, and practical, because it usually contains only few instructions. It does make sense, however, if you manage to think like a mathematician again, for a second.
What monads do is they are patterns of programming that allow you to describe a program. Let us imagine a program that allows a user to open and view a file from some cloud storage. Let’s use Google Drive. You can describe what the program should do in a series of steps:
- Check if there is internet
- Access the credentials for Google Drive
- Connect to the server
- Query the file
- Get the file
- Parse the file
- Display the file to the user
In monadic code, you could write it as simply as such:
const result = Some('/path/to/file.txt')
.bind(check_internet)
.bind(get_credentials)
.bind(connect_to_server)
.bind(access_file)
.bind(download_file)
.bind(parse_file)
.run()
if (result instanceof Some) {
console.log('Computation successful', result.value)
} else if (result instanceof None) {
console.log('Something went wrong')
}
As you can see: Very easy to understand (as some would say, “reason about”), because the code tells you exactly what it does. Only at the end, the result may be either Ok or Err (or Some or None, depending on which monad type you personally decided to use).
“But there is a clear order to the computations!,” you may now think. And indeed, we can’t just arbitrarily move around the various functions we have chained up. If we parse a file before we actually download it, there won’t be much to parse. But this is still a monad. Why? Because the Option monad doesn’t care about the value.
The beauty of this approach now becomes apparent once you think about each of these steps. For example, you may support multiple cloud storage providers, not just Google Drive. In that case, the get_credentials function gets more complex. It itself may call a variety of functions, such as checking some database where multiple credentials are stored, or checking a file on disk. The list goes on. However, it may be that get_credentials does not need to run, because there is no internet. Does the get_credentials function need to understand that? Not at all. Indeed, here’s how you can implement this using monads:
function get_credentials (path: Option<string>): Option<Credentials> {
if (path instanceof None) {
return None()
} else {
return fetch_for_path(path) // This itself takes an Option and returns one.
}
}
As you can see, the function get_credentials is now pure in the definition introduced earlier, because it doesn’t have any side effects. The side effects kind of “hide” within the Option. This reinforces the thought introduced earlier: Monads happily take all kinds of side effects. What they do is not actually produce purely “pure” code. There will always be errors. It’s just that many functions can be implemented without any error handling, as that will be passed on to the caller. It is really just about ensuring that errors, if they occur, are moved into a space in your program where you can deal with them properly. Instead of throwing errors randomly across your code base, you can define a central place where you want all errors to end up, and then you can handle them appropriately.
Indeed, given that this other function, fetch_for_path also takes an option and returns an option, you can completely get rid of this instance check. This then gives you some additional space to check for more meaningful things, for example: Should we grab Google Drive credentials, or rather iCloud credentials?
function get_credentials (path: Option<string>): Option<Credentials> {
if (is_on_google_drive(path)) {
return get_gdrive_creds()
} else if (is_on_icloud(path)) {
return get_icloud_creds()
}
}
Note that this really doesn’t describe deferred computing. We don’t set some timeout after which the code will actually run. From the computer’s perspective, it will absolutely run. But from the perspective of us reading this code, it will only run once we call the run function. I feel this is helpful.
A Better Explainer of Monads
So, what are monads? It’s basically a way of directing your traffic where you want it. Instead of throwing errors, or raiseing them (like in JavaScript or Python), you wrap your computations in some container that keeps them “floating” in the air, until you call the run function. In Rust, you usually unwrap() them. This will actually commence the computation. And this means that, if some error occurs, it will be delivered to the code that calls the unwrap. If you have a function that you don’t want to deal with any error, you just return such a monadic type, too.
All of this basically allows you to organize your programs differently than what we all have learned with so many programming languages. Instead of erroring left and right, you control what happens. This can be useful, but it is not necessary.
This leads me to a final section:
Conclusion: You Probably Don’t Need Monads
Most of us very likely never need any monads. In fact, I think there are only three cases in which using monads makes actual sense:
- You write in a programming language where monads are first-class citizens (Rust, Haskell).
- You want to explicitly move any error-handling code to a particular part of your program, and not rely on throwing and catching errors at specific parts of the application.
- The program has been built on top of the idea of monads.
I would argue that points 2 and 3 are not always a given. I have seen very large programs that run without a single monad. So it’s more about preference rather than necessity, even for large programs.
Don’t Use Monads Unless You Need to
In fact, I don’t think you even should use monads unless you absolutely need to. If your language of choice defines a way to throw errors, this is a good indicator that it was written without monads in mind. Plugging monads into such a language is certainly possible, but what is the point? If you come from math and have learned to think in such a “monadic” way, then absolutely, go ahead, but if not, I feel it makes everything more complicated.
This leads me to the type of code I usually write: Data analysis pipelines. This is specific code that has one particular property that make monads useless. Completely useless. And that is that data analysis code is required to run without errors. Any “side effect” such as some data column not being present or some file being missing is a complete stop for your program. The only Option you have then is to abort any further processing because the data required for anything afterwards is not present. Your program is useless without the data, or side effects. So you don’t need to go through the hassle of implementing monads, because you never want the computation to return None, and, if it does, your code is wrong.
Now, if you decide to use Haskell to do your statistics, then be my guest. But Haskell implements monads already, so it’s simple to use them. Not using monads would mean to work against the language. But in any other case, raise errors liberally. Because R, Python, and many other languages, have been designed with the error throwing idea, not with monads. And for data analysis code, it’s really okay never to use them.
Never Work Against the Language
And this is why monads are so hard to grasp: Most programming languages haven’t been built with monads in mind, and since it’s just one way of writing code that can fail, it is just as fine to simply throw errors, especially if the language provides them. Writing monads means to add a lot of code that is not strictly necessary.
However, if you write in a language that implements monads as first class citizens, such as Haskell or Rust, then you absolutely need to understand monads, because otherwise you would work against the language by not using monads.
The result of working against the language is code as you can see with my first experiment of writing Rust code. I am so used to the error handling patterns of other languages that I subconsciously forced my Rust code to check errors every time, instead of allowing to pass them along. Take this function that can switch an audio device:
pub fn switch_device (&mut self, device_index: usize) {
let (stream, config, rx, real_index) = create_stream(Some(device_index), None);
self.sample_rate = config.sample_rate.0;
self.stream = Some(Box::new(stream));
self.thread_recv = rx;
if self.event_sender.is_some() {
self.event_sender.as_ref().unwrap().send(AudioEvent::InputDeviceChanged(real_index)).unwrap();
}
}
After understanding monads, I suddenly saw what violence I was doing to the language here. Specifically, the function never returns anything. Instead, what can go wrong — that is, the actual switching inside the if-statement — is immediately unwrapped. This means that, if something goes wrong, the program will crash completely. And not give me any error. Instead, I should’ve removed all of these various unwraps in the code, and returned a Result instead. Then, my main application which calls this function would’ve had to actually look at the result, and do some more proper error handling than simply … well, crashing the entire app, just because I couldn’t switch the audio device. But as it stands now, my main code will literally just call this function, and ignore whatever is going on with it.
As you see: If you think in terms of try-catch-constructs, you will fail at writing proper Rust or Haskell code. But, likewise, if you want to force monads into a language that does not have an understanding of them, you will just as well work against the language.
In short: If you don’t know whether you need monads – you probably don’t.
1 Taken from a comment under the Computerphile video on monads. Also found in this blog post by Chris Rybicki.
2 This article has originally been written in May 2025, but because I wanted to improve it a bit before publication, I let it simmer for some time. Some parts may still refer to me as a PhD student, but I am happy to announce that, at the time of writing, I am no longer a PhD student.
3 Interestingly enough, this is no longer true. When I first wrote this paragraph in May 2025, my spell checker has annotated most words in that paragraph, but now it only complains about “monadic.”
4 Of course none of these terms are “fuzzy” because they are mathematical and have well-defined meanings. This was a joke.
5 I may write an article on that later.
6 I’m intentionally vague with the concepts of Haskell or their names, because I’m not familiar with it, albeit my friend Albert Krehwinkel has, at times, tried to make me familiar with Pandoc’s source code.