2

I'm trying to work through how to write this code.

def get(params):
    """ Fetch a user's details, or 404 """
    user = User.fetch_by_id(params['id'])
    if not user:
        abort(404)
    # Render some template for the user...

What's the best way to handle the case where the lookup fails? One principle says you should avoid returning null values from functions. These lead to mistakes and AttributeErrors etc. later on in the file.

Another idea is to have fetch_by_id raise a ValueError or similar if no user exists with that id. However there's a general principle that you shouldn't use exceptions for control flow, either, which doesn't help much.

What could be done better in this case?

1
  • Research the Null Object Pattern. Jun 26, 2013 at 7:44

3 Answers 3

2

I won't be against the first approach (returning null) too much, but will rename the method to clearly indicate it may return null.

Signatures like:

findCustomer(int customerId)
parse(string number)
pop(stack sequence)

are not clear. If a customer doesn't exist, is the method returning null or throwing a CustomerNotFound exception? If the number is "abc" and cannot be reasonably parsed, is the method returning null or throwing InvalidFormat exception? What happens when poping an element from an empty stack?

Instead:

findCustomerIfAny(int customerId)
tryParse(string number)
lastOrNull(stack sequence)

are easier to understand.

Other options you have include:

1. out parameters

If your language supports out parameters, a different approach would be to have the following:

tryFindCustomer(int customerId, out customer result) returns bool

The boolean result indicates whether the customer was found or not.

Personally, I find this approach too verbose and ugly. I won't use it.

2. Tuples

If your language has tuples, then you can write:

findCustomer(int customerId) returns (bool isFound, customer match)

This is perfectly valid, and may make your code extremely explicit.

3. Value or default

There is also another interesting convention in LINQ in .NET. A method which searches for the first element of a sequence, FirstOrDefault, returns the default value if the sequence is empty.

This means that:

[ 2, 7, 1 ].FirstOrDefault()

gives 2, while:

[].FirstOrDefault()

gives 0, since default(int) is 0. Note that:

default(customer) ↔ null
default(System.Drawing.Point) ↔ System.Drawing.Point.Empty ↔ (0, 0)

etc.

Unfortunately, you can't override default operator, which makes this approach rather limited.

I'm not sure this last approach will help in your case, but it's worth mentioning.

4. Status within the returned value

A custom-made approach would be to integrate a status value within the class itself. For example:

// `findCustomer` always returns a non-null value.
selectedCustomer = findCustomer(customerIdFromInput)

// When the customer cannot be found, `IsEmpty` is set to `true`.
if (selectedCustomer.IsEmpty) {
    throw CustomerNotFoundException;
}
1

Renaming the methods as MainMa suggested is a possibility, though the resulting method names may not be appealing to everyone. In the line of your question on how to actually avoid returning null, here are two more options:

Option/Maybe

Several languages directly support a type that encodes whether you successfully found an object or not (f.ex. Option in Scala, or Maybe in Haskell). With these you basically wrap your result inside of another type to ensure you deliver a non-null result. More precisely, if you found the user object, you dont returnuserbutSome(user), whereSomeis anOption. If you did not find the user, you instead return aNoneobject, which is better than anull, as you at least have theOption` interface available on it.

For languages, which do not directly support these, there are typically additional libraries you can pull in for that (for example Optional in guava for Java).

Null Object Pattern

A different option is the null object pattern. Essentially, it means that you have to return a User object, no matter what happens. In case you could not find a user object for your id, you have to return a specially crafted null-user object. The advantage of this object is that it implements the complete interface of a User class and callers need not care about whether the user was found or not (they may want to though, which is why it makes sense to add some isNull method or such to the interface).

Say, you want to write code that given a user id somehow e-mails that user about your oh-so-great new promotion. With the null object pattern you could retrieve the user via id, but instead get the null-user object. On that one you can simply call some getEMailAddress method or such, which may return you a fallback address like [email protected], or anything else that suits you. Your complete code does not need to check whether a user with that id even exists.

2
  • His code is in Python, Guava isn't available. Unless of course he is coding in Jython! :)
    – Chiron
    Jun 26, 2013 at 7:37
  • @Chiron: that was just an example, and of course, Option is a simple monad you can implement yourself just as well.
    – Frank
    Jun 26, 2013 at 8:58
0

I'm trying to work through how to write this code.

def get(params):
  """ Fetch a user's details, or 404 """
  user = User.fetch_by_id(params['id'])
  if not user:
    abort(404)
  # Render some template for the user...

What's the best way to handle the case where the lookup fails? One principle says you should avoid returning null values from functions.

Eh... IMO there are only two reasonable options for a direct lookup like 'find_by_id'. Everything else is overly complicated.

1) return something false-y, like null
2) raise an exception

Returning null is slightly simpler when the caller will immediately check the return value. Throwing an exception is simpler when the caller wants to perform multiple actions and handle failures identically.

IMO a generic API should provide both for single record retrieval, e.g. 'find_by_id', which may return null, and 'fetch_by_id' which returns a record or raises an exception.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.