Object#try is quite a commonly used method in Rails applications to cover cases where there is a possibility of dealing with a
nil value or to provide flexible interface for handling cases where some kind of object doesn’t necessarily implement given method. Thanks to
try, we may avoid getting
NoMethodError. So it seems like it’s perfect, right? No
NoMethodError exception, no problem?
Well, not really. There are some severe problems with using
Object#try, and usually, it’s quite easy to implement a solution that would be much better.
Object#try – how does it work?
The idea behind
Object#try is simple: instead of raising
NoMethodError exception when calling some method on
nil or calling a method on non-
nil object that is not implemented by this object, it just returns
Imagine that you want to grab the email of the first user. To make sure it won’t blow up when there are no users, you could write it the following way:
What if you implemented some generic service where you can pass many types of objects and, e.g., after saving the object it attempts to send a notification if the object happens to implement a proper method for that? With
Object#try it could be done like this:
1 2 3 4 5 6
As you can see, it is also possible to provide
arguments of the method.
What if you need to do some chaining of the methods where you can get
nil at each intermediate step? No problem, you can use
What is the problem then?
Object#try is capable of handling multiple cases, so what is the problem with using it?
Well, there are many. The biggest issue with
Object#try is that in many cases it
solves problems that should never happen in the first place and that problem is
nil. The another one is that the intention of using it is not clear. What does the following code try to say?
Is it a legit case that some payment might not have a client and indeed it could be
nil? Or is added “just in case” if
client happens to be
nil to not blow up with
NoMethodError exception? Or even worse, does
client happen to be a polymorphic relationship where some models implement
addresses method and the others don’t? Or maybe there is a problem with data integrity, and for a few payments the client was deleted for some reason, and it’s no longer there?
Just by looking at this code it is impossible to tell what’s the intention of
Object#try, there are just too many possibilities.
Fortunately, there are plenty of alternative solutions that you can apply to get rid of
Object#try and make your code clear and expressive – thanks to that, it will be much more maintainable, more readable and less prone to bugs as the intention will no longer be ambiguous.
Here are few “patterns” you could apply depending on the context where
Object#try is used.
Respecting Law of Demeter
Law of Demeter is a handy rule (I wouldn’t go that far to call it a “law” though) which helps avoid structural coupling. What it states is that hypothetical object A should be only interested in its own immediate surrounding and should not be aware of the internal structure of its collaborators or associations. In many cases, it means having only one “dot” in method calls. However, Law of Demeter is not really about the amount of “dots” (method calls), it’s only about the coupling between objects, so chained operations and transformations are perfectly fine, e.g., the following example doesn’t violate the law:
but the following one does:
Respecting Law of Demeter usually results in a clean and maintainable code, so unless you have a good reason to violate it, you should stick to the law and avoid tight coupling.
Let’s get back to the example with
address. How could we refactor the following code?
The first thing would be to reduce structural coupling and implement
1 2 3 4 5
It’s much better now – instead of referring to the address via
payment.client.try(:address) we can simply do
payment.client_address, which is already an improvement as
Object#try happens only in one place. Let’s refactor it further.
We are left now with two options: either
nil is a legit case or not. If it is, we can make the code look confident and explicitly return early, which clearly shows that having no
client is a valid use case:
1 2 3 4 5 6 7
If it never happens to be
nil, we can skip the guard statement:
1 2 3 4 5
Such delegations are pretty generic; maybe Rails has some nice solution to this problem? The answer is “yes”!
ActiveSupport offers a very nice solution to the exact issue:
ActiveSupport#delegate macro. Thanks to that macro, you can define delegations and even handle
nil in the exact way we did it.
The first example, where
nil is a legit use case, could be rewritten the following way:
1 2 3
and the second one, if
nil is never to be expected:
1 2 3
Much cleaner, less coupled and we’ve managed to achieve the final result of not using
Object#try, but just in a much more elegant way.
However, it is still possible that we might not expect payment to have an empty client (e.g. payment for the transaction that is not completed yet) in some cases, e.g., when displaying data for the payments with completed transactions, but somehow we are getting dreaded
NoMethodEror exception. It doesn’t necessarily mean that we need to add
allow_nil: true option in
delegate macro and for sure it doesn’t mean that we should use
Object#try. The solution here would be:
Operating on the scoped data
If we want to deal payments with completed transactions, which are guaranteed to have
client, why not simply make sure that we are dealing with the right set of data? In Rails apps that would probably mean applying some ActiveRecord
Payments collection, like
1 2 3
Since we never plan to do anything with client’s
address for payments for not completed transactions, we don’t need to explicitly handle
Nevertheless, even if
client were always required for creating a payment, it would still be possible that such code might result in
NoMethodError. One example where that might happen would be a deleted by mistake associated
client record. In that case, we would need to fix:
Ensuring data integrity, especially with RDBMS like PostgreSQL, is quite simple – we just need to remember about adding the right constraints when creating new tables. Keep in mind that this needs to be handled on a database level, validations in models are never enough as they can easily be bypassed. To avoid the issue where
client turns out to be
nil, despite presence validation, we should add
NOT NULL and
FOREIGN KEY constraints when creating
payments table, which will prevent us from not having a client assigned at all and also deleting the client record if it’s still associated with some payment:
1 2 3
And that’s it! By remembering about those constraints, you can avoid a lot of unexpected use cases with
Ensuring types via explicit conversion
I saw few times
Object#try used in a quite exotic way which looked similar to this:
Well, this code clearly shows that some string is expected to be found under
name key in
params, so why not just ensure it is a string by applying explicit conversion using
Much cleaner that way!
However, those two codes are not equivalent. The former one returns a string if
params[:name] is a string, but if it is
nil, it will return
nil. The latter always returns a string. It is not entirely clear if
nil is expected in such case (which is the obvious problem with
Object#try), so we are left with two options:
nilis the expected return value if
nil– might not be the best idea as dealing with nils instead of strings might be quite inconvenient, however, in some cases, it might be necessary to have
nils. If that’s the case, we can make it clear that we expect
nilby adding a guard statement:
1 2 3
- a string is the expected return type – we don’t need to bother with guard statements, and we can just keep the explicit conversion:
In more complex scenarios, it might be a better idea to use form objects and/or have a more robust types management, e.g. by using dry-types, but the idea would still be the same as for explicit conversions, it would just be better as far as the design goes.
Using right methods
Dealing with nested hashes is quite a common use case, especially when building APIs and dealing with user-provided payload. Imagine you are dealing with JSONAPI-compliant API and want to grab client’s name when updating. The expected payload might look like this:
1 2 3 4 5 6 7 8 9
However, since we never know if the API consumer provided a proper payload or not, it would make sense to assume that the structure won’t be right.
One terrible way to handle it would be using… guess what? Obviously
It’s certainly hard to say that this code looks pleasant. And the funny thing is that it is really easy to rewrite cleanly.
One solution would be applying explicit conversions on each intermediate step:
That’s better, but not really expressive. Ideally, we would use some dedicated method. One of those potentially dedicated methods is
Hash#fetch which allows you to provide a value that should be returned if the given key is not present in the hash:
It looks even better but would be nice to have something even more dedicated for digging through nested hashes. Fortunately, since Ruby 2.3.0, we can take advantage of
Hash#dig, which was implemented for exactly this purpose – digging through nested hashes and not raising exceptions if some intermediate key turns out to not be there:
Having Proper interfaces / Duck typing
Let’s get back to the example that was mentioned in the beginning with sending a potential notification:
1 2 3 4 5 6
There are two possible solutions here:
- Implementing two set of services – one that sends notifications and one that doesn’t:
1 2 3 4 5 6 7 8 9 10 11 12
Thanks to this refactoring, the code is much cleaner, and we easily got rid of
Object#try. However, now we need to know that for one type of objects we need to use
MyServiceA and for another type
MyServiceB. It might make sense, but might also be a problem. In such case the 2nd option would be better:
- Duck typing. Simply add
send_success_notificationmethod to all objects that are passed to
MyServiceand if it’s supposed to do nothing, just leave the method body empty:
1 2 3 4 5 6
The extra benefit of this option is that it helps to identify some common behaviors of the objects and to make them explicit. As you can see, in case of
Object#try a lot of domain concepts might stay implicit and unclear. It doesn’t mean they are not there; they are just not clearly identified. This is yet another important thing to keep in mind –
Object#try also hurts your domain.
Null Object Pattern
Let’s reuse the example above with sending notifications after persisting some models and do a little modification – we will make
mailer an argument of the method and call
send_success_notification on it:
1 2 3 4 5 6
That’s going to work great if we always want to send a notification. What if we don’t want to do it? One terrible way to handle it would be passing
nil as a mailer and take advantage of
1 2 3 4 5 6 7 8 9
But you’ve probably already guessed this solution is a no-go. Fortunately, we can apply Null Object Pattern and pass an instance of some
NullMailer which implements
send_success_notification method that simply does nothing:
1 2 3 4 5 6 7 8 9 10 11 12 13
That’s certainly better than using
&. a.k.a. lonely/safe navigation operator?
&., lonely/safe navigation operator is a pretty new thing introduced in Ruby 2.3.0. It’s quite similar to
Object#try, but it’s less ambiguous – if you call a method on the object different than
nil, and this method is not implemented by that object,
NoMethodError will still be raised which is not the case for
Object#try. Check the following examples:
1 2 3 4 5 6 7 8 9 10 11
Does it mean safe navigation operator is fine and safe to use? Not really. It still comes with the same problems as
Object#try does, it’s merely one serious issue less.
Nevertheless, I think there is a case where the lonely operator is not that bad. Check the following example:
1 2 3 4 5
What we want to do is create a comment belonging to some
current_user who might be an author and also assign a
current_user, who might be nil.
The same code could be written as:
1 2 3
or maybe as:
1 2 3 4 5 6 7 8
But I think neither of those alternatives is more readable than the first example with
&. operator, so might be worth trading a bit of clarity for more readability.
I believe there is not a single valid use case for
Object#try due to the ambiguity of its intentions, negative impact on the domain model and simply for the fact that there are many other ways to solve the problems that
Object#try “solves” in a clumsy way – starting from respecting Law of Demeter and delegations, through operating on properly scoped data, applying right database constraints, ensuring types using explicit conversions, using proper methods, having right interfaces, taking advantage of duck typing, ending with Null Object Pattern or even using the safe navigation operator (
&.) which is much safer to use and might be applied in limited cases.