Using ! Wisely with Rails Development
As Ruby programmers know, a method name ending in ! can hold a variety of meanings, depending on the source. In plain ol’ Ruby, methods that end in a bang mutate an object in place instead of returning a modified copy of the object. In libraries such as AASM, such methods indicate that the method persists the state of the object to your database. Some people use them to denote a dangerous method. My personal favorite is the Rails connotation, which indicates a method that raises on failure (as opposed to failing without blowing up the chain of execution).
In this post, I’ll explain why I choose the standard Rails connotation for professional Rails development and about how consensus on the usage of a single punctuation mark can make a significant impact. As a caution, the following is a specific suggestion for Rails app development and not necessarily for Ruby development at large.
Option 1: ! to mean raise on failure
Before delving into a comparison, it’s important to set ground rules for comparison across these conventions. Good qualities in a convention are:
- Applicability to commonly found scenarios, not just edge cases
Objectivity is important because it helps build a shared language across the members of your team and makes collaboration more efficient. Focus on applicability to commonly found scenarios maximizes the efficiency by choosing a convention that deals with a common theme in your codebase.
The proposed convention fits both of these criteria very well. It’s objective — you know exactly what will happen if you call a ! method and it fails. Furthermore, you can encounter both sides of this convention very frequently in your code, which I’ll give examples of now.
Methods that raise exceptions on failure tell you that something went seriously wrong in your business logic flow and rescuing is necessary to clean up the mess you made. For example, let’s say you have an e-commerce app and a user makes an order for a bar of soap. You’ve told the user that you have the inventory to fulfill the order, but actually, the last bar was shipped due to a glitch in your inventory system! (Oops.) Your fulfillment code might look like this:
items = order.get_items_from_factory_in_order(order)
raise NotAvailableError if items.empty?
orders.each do |order|
rescue NotAvailableException => e
# notify engineering
There are many instances where you don’t want your methods to raise on failure. I generally like to use non-! methods for things like APIs, where you have to communicate failure back to the client. An excellent example is validation:
controller DocumentsController < BaseController
document = Document.new(params)
render document.errors # throw the errors back to the client
In this scenario, one of the normal flows of your business is failure. Clients are expected to pass up invalid addresses and failure is an expected communication back to the client.
Both of these scenarios are quite frequently found in the daily course of programming, can be instantly recognized, and give the developer reading this code a piece of the expectation of how the code will work. Now that I’ve explored the reasons why this convention works, let’s see what the alternatives have to offer and where they might fall short.
Option 2: ! to mean modify object in place
Anecdotally, I rarely find methods where the object returns a modified copy of the original, with the exception of basic Ruby data structures like hashes or arrays. A method that returns a modified copy implies that a need for keeping the original object in memory is necessary. In my experience, this need is uncommon and seems to be the exception. This convention makes sense for basic Ruby objects, like hashes and arrays, for sure. But with instances of Active Model records, we almost never see methods that give us the option of returning a copy vs mutating the original object. As an example, if you were to differentiate the methods `User#set_first_name` vs `User#set_first_name!`, the former would almost never be encountered. Copies of Active Model instances are rarely necessary except when creating records for auditing, which is typically a concern shared by a small minority of your code.
Service objects — since they are more varied in purpose — have more potential to return unmodified copies of themselves. However, I’ve found this to be the exception more than a rule. Most service objects tend to encapsulate an action or set of behavior — factories or query methods, for example — typically not holding a state, which makes this convention non-applicable here.
Option 3: ! to mean dangerous
Across many Rails firms I’ve worked for, it’s very common practice to say ! means “danger, this method should be invoked with caution”. Consider all writing of data and mutative operations dangerous and know their repercussions. Additionally, the dangerousness of a method can change over time. A dependency that is not dangerous today could be dangerous tomorrow. As an example, the innocuous changing of a timestamp could be used by another class for a critical function. Or perhaps it’s surfaced in a part of the app you wouldn’t expect. For these reasons, introducing the possibility for developers to invoke methods that they consider “not dangerous” is encouraging bad behavior — to program without thinking about consequences.
Option 4: ! means persist
I don’t encounter many situations in the wild where I want to set a value and not persist it. AASM is the only library I know of that follows this convention and could possibly make sense in the state machine world. For example, flowing through multiple statuses (state A to B to C to D) without persisting (until the end) might be a good use case. However, even if I did have a need for this behavior, it would be a very minor part of my code.
Option 5: Not using ! at all
One possible solution is to omit ! in method definitions. This will avoid confusion across frameworks and domains. The alternative might be to pass `raise: true` as a parameter to methods within your app. But this convention throws out a useful tool that Ruby gives you. A consensus packs more meaning into your methods and allows for more efficient dialogue between developers on your app.
Conclusion: possible downsides
There are downsides with this approach, of course. Primarily, you don’t get the wins from the other possibilities. Second, in a real world app, you need to consider what happens if your method must call two methods and only one of them raises on failure. The first issue is unavoidable no matter which convention you choose, but the second issue is solvable. A contract between developers that you can call a method and have knowledge of what will happen on failure is what you get in return. When you invoke these methods, you should investigate the contents of the functions you’re calling of course, but having a good convention in ! methods makes for easier reading and sets up expectations for other coders. I’m going to write a follow-up article on how to make this happen in practice within your app.
Thanks to my friend Spencer Horstman for helping me edit this article.