I recently stumbled upon Scott Wlaschin’s talk on railway oriented programming where he talked about an epic new way of handling errors using the functional approach. In this lecture, he uses a railway track as an analogy to give developers a better understanding of the pattern. The philosophy itself isn’t directly related to programming, but it can help you improve your codebase.
Railway oriented programming is a functional approach to the execution of functions sequentially. I will be using error handling as the case study here. Apart from error handling, there are various other applications for the railway oriented pattern in general.
The main point is that your function can only return either a success or a failure. Failure should be handled using the throw
statement so as to throw an exception, while success is what leads to another function, which can be of any type.
This style of error handling uses monadic behavior — a substitute way of handling errors. One thing I really like about this style is the elegance and readability it provides to your codebase.
It is nearly impossible these days to have a program that doesn’t need to be handling errors. Even the simplest of programs need error handling, from validating users’ input details, network issues, handling errors during database access, and so many related situations that can crop up while coding.
Back to what railway oriented programming really is. Below is a visual representation of what this looks like:
In a simpler form, every method or function either yields a success or an error (the word failure sounds cooler to me, though.)
In a real-world application, we might also want to move from error to success. This is called self-healing in Node.js, for example.
From my understanding, I have found various applications for the railway oriented pattern that go beyond error handling. One is the control flow. This idea incorporates interactivity into your application, thereby providing conditionals.
Now, let’s get deeper into the specifics of this pattern. Ultimately, railway oriented programming boils down to two options: the happy path and the unhappy path.
Let’s imagine we want to read the content of a file and send it as an email to a customer. In order to successfully complete this task, the customer’s email must be valid, and it has to have a complete name.
# Happy Path > read file > get email address > get firstname and lastname > send email
Where:
const sendWayBillMail = async() => { const data = await fs.readFile('emailContent.txt', 'binary') const { emailAddress, firstName, lastName } = await User.findById(userId) sendMail(emailAddress, firstName, lastName, data) return 'Done' }
There you have it. This makes us happy. This looks ideal, but in real life it isn’t perfect. What if we don’t get the specific outcome we want? What if the file is invalid? What if our firstName
wasn’t saved? What if? What if? Now, we’re getting pretty unhappy here. There’s plenty of things that could potentially go wrong.
An example of an unhappy path would be this:
const sendWayBillMail = async() => { const data = await fs.readFile('emailContent.txt', 'binary') if (!data){ return 'Empty content or invalid!' } const { emailAddress, firstName, lastName } = await User.findById(userId) if (!emailAddress) { return 'Email address not found!' } const isValidated = await validateEmail(emailAddress) if (!isValidated) { return 'Email address not valid!' } if (!lastName) { return 'Last name not found!' } if (!firstName) { return 'First name not found!' } sendMail(emailAddress, firstName, lastName, data) return 'Done' }
The unhappy path grows faster than unexpected. First, you think the file read could be empty or invalid. Then, you see that the isValidated
response might be a fail. Then you remember you need to check for a null email. Then you realize the lastName
must not be there, and so on.
Finding the unhappy paths is always quite a challenge, which is extremely bad for building software. You might wake up to a series of bug reports in your inbox from your users. The best thing to do is to always put your feet in your users’ shoes.
The main goal of railway oriented programming is to ensure every function or method should and must always return a success or a failure. Think of it like a typical railway track — it either goes left or right.
The main idea is to tackle the happy path as if it’s the main path — it should be where you’re normally heading. In the image below, it’s the green track. If there is a failure, we move to the error track. In our case, it’s the red track.
We stay on this track until the error is dealt with using recovery, which shifts the flow back to the main track.
Through this method, we push error handling to where it belongs and control the flow of exceptions while creating a pipeline. Everything moves on the green track if there is a happy outcome, and if we get an unhappy outcome, it switches to the red track at that instant and flows to the end.
So, how do we apply this to our current code? The main idea of ROP, again, is to create several functions that can switch between the two tracks while still following the pipeline.
This ‘switches’ idea is what brings about the two track system:
In our code, we already have the validateEmail
function, so we just apply the switch to it by adding if/else. If/else will handle the success and failure functions.
const validateEmail = async (email) => { if (email.includes('@')) Success else Failure }
However, the above code syntax is not correct. The way we illustrate the success and the failure is through the green and red track.
This outlook requires us to implement every task as a function, which yields no interfaces except for one. This provides much better code maintainability and control over the application flow.
const sendWayBillMail = async(file) => { const data = await readFile(file) const { emailAddress, firstName, lastName } = await User.findById(userId) const response = await checkForNull(emailAddress, firstName, lastName) const isValidated = await validateEmail(response.emailAddress) sendMail(response.emailAddress, response.firstName, response.lastName, data) return 'Done' }
In each of these functions, we then handle errors as they should be handled, which is through the two track unit. The above code can still be refactored to achieve simplicity and reliability.
It’s important to keep in mind that the railway pattern is an orientation or design style. It’s less about the code itself, and it’s more about applying the pattern to your code to improve efficiency and reliability.
In general, patterns have advantages as well as disadvantages. That being said, you should consider railway oriented programming as a choice you make for your code rather than a rule you always have to follow when building an application.
Deciding how to carry out error handling is a matter of perspective, which is why we have the railway oriented pattern.
If you are going to choose to utilize railway oriented programming, here are some of the benefits you’ll see:
The above advantages will ultimately improve your codebase. It comes with test-driven development and does not affect the performance of your application.
This article helps you wrap your head around the idea of the “parallel error handling” technique. You can get more information about this method by checking out Scott Wlaschin’s full lecture on the pattern.
Railway oriented programming gives us a sense of our validation as an independent function, creating two results for our pipeline. Now you can apply this method to handle the happy and unhappy paths in your code in a clean and functional way.
Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBackdrop and background have similar meanings, as they both refer to the area behind something. The main difference is that […]
AI tools like IBM API Connect and Postbot can streamline writing and executing API tests and guard against AI hallucinations or other complications.
Explore DOM manipulation patterns in JavaScript, such as choosing the right querySelector, caching elements, improving event handling, and more.
`window.ai` integrates AI capabilities directly into the browser for more sophisticated client-side functionality without relying heavily on server-side processing.
5 Replies to "What is railway oriented programming?"
In the last example, wouldn’t you still need to check isValidated before calling sendMail(), or maybe passing it in would be more in line with this design pattern? Or maybe I’m missing something, its late. 🙂
Its is really unclear how the last example allows for decomposition of a single failure/sucess value to multiple firstname, lastname values
Am I missing something? This seems like a basic overview of the Maybe Monad with respect to exceptions, but without any of the work shown. You haven’t given any mechanisms for ‘self-healing’, or dealing with the unhappy path at all, just that you CAN switch to one and that it’s better to write small functions than to throw all error handling into the main function.
He already stated that the code can be optimized to simplicity and reliability. The message is passed.
The principles of this “railroad” concept is nothing new. I was taught error handling like this over 30 years ago.
One minor suggestion – rather than “success” or “failure”, the reality is that there is success, recoverable failure, non-recoverable unit failure, and non-recoverable app failure. The developer needs to determine if a failure is recoverable (e.g. login failure, connection timeout, etc.) or non-recoverable. If the latter, the developer needs to decide if the app is now unusable, or just one portion of it.
Another suggestion – don’t just return success or failure. Include the appropriate enum flag, but also include in the return object details about what went wrong, such as all the error messages in the exception stack, snapshot values of runtime variables, what module, method, and line number the error occurred, etc., not only for logging, but for useful information for the user in a recoverable failure.