3

First of all, I’m not sure whether the topic belongs to StackOverflow or not. However, I’ve found some discussions regarding other programming languages that addresses the topic on StackOverflow but not for Javascript. So, I’m just going to throw my question into the ring:

What is the best way/pattern to handle a possible „not found“ scenario in hypothetical GetUserByUsername function in Javascript.

Why I’m asking? I want to follow a certain coding standard throughout my project.

My more specific scenario the following:

  • I have class User
  • I have a list of user which contains instances of the class User
  • I look up for a certain user but this user doesn’t exist in the list

How should I handle that “not find” situation in my regarding function?

I found the following approaches:

  • Return undefined
  • RORO (Receive an object, return an object)
  • Null object pattern
  • Throwing a custom error

So what do I mean exactly. Taking the following example class User.

module.exports = class User {
  constructor() {   
    this.userName = null,
    this.name = null,
    this.age = null
  }
  ...
  getName() {
    console.log(this.name);
  }
}

Somewhere in my code I have a function that provides the functionality to return a certain user by its user name from a list of users. That list is an array what consists of instances of the class User.

A simple implementation could like that:

{
  ...

  getUserByUsername(pUserName) {
    /* listOfUsers array with objects of the class User */
    var lUser = this.listOfUsers.find(user => user.userName === pUserName);
    return lUser;
  }
  ...
}

In some case it could be possible that the passed user name doesn’t belong to a user. Therefore no user is found.

How could I handle it?

1.) Return undefined - like the example code does.

/* We expect "Unknown" is not a user name in that list*/
var lRequestedUser = getUserByUsername("Unknown");

/* Check if requested user is undefined. Note: I'm using lodash for staff like that */
if(_.undefined(lRequestedUser)) {
  //Handling 
}

lRequestedUser.getName();

2.) RORO (Receive an object, return an object) I’m passing an object to my function and I’m returning an object as result. Within the object I’m returning, I have an indicator what shows if the operation was successful.

getUserByUsername(pParams) {
  if(_.undefined(pParams.userName)) {
    //Throw custom error 
  }

  var lReturnObject = {
    isSuccessfull: false,
    data: null
  }

  /* listOfUsers array with objects of the class User */
  var lUser = this.listOfUsers.find(user => user.userName === pParams.userName);

  if(!(_.undefined(lUser))) {
    lReturnObject.isSuccessfull = true;
    lReturnObject.data = lUser
  }
  return lUser;
}
...
}

/* 
Building a object which is passed to the function. Username is a member of that object
 */
var lRequest = {
  userName: "Unknown"
}

/* We expect "Unknown" is not a user name in that list*/
var lRequestedUser = getUserByUsername(lRequest);

if(!(lRequestedUser.isSuccessfull)) {
//Handling 
}

lRequestedUser.data.getName();

3.) Null object pattern The thing I don’t like about that solution is the fact, that I always have to enhance the class for the null objects if the main class gets an additional function.

  module.exports = class NullUser {
      constructor() {   
        this.userName = null,
        this.name = null,
        this.age = null
      }
      ...
      getName() {
        console.log(this.name);
      }
    }


{
  ...

  getUserByUsername(pUserName) {
    /* listOfUsers array with objects of the class User */
    var lUser = this.listOfUsers.find(user => user.userName === pUserName);

    if(_.undefined(lUser)) {
      return new NullUser();
    }

    return lUser;
  }
  ...
}


/* We expect "Unknown" is not a user name in that list*/
var lRequestedUser = getUserByUsername("Unknown");
lRequestedUser.getName();

4.) Throwing a custom error

{
  ...
  getUserByUsername(pUserName) {
    /* listOfUsers array with objects of the class User */
    var lUser = this.listOfUsers.find(user => user.userName === pUserName);

    if(_.undefined(lUser)) {
      throw new CustomError(...);
    }

    return lUser;
  }
  ...
}

/* We expect "Unknown" is not a user name in that list*/
try {
  var lRequestedUser = getUserByUsername("Unknown");
  lRequestedUser.getName();
} catch (e) {
  if (e instanceof CustomError) {
      //Handling
  }
}

Personally I prefer the option to return an object, which contains an indicator, and the option of throwing a custom error. The example is synchronous. In my projects I also have asynchronous code. I’m using promises for that.

So any thoughts that would be the “best” solution or the way I should prefer?

2 Answers 2

1

The answer is, in my opinion, it depends! All the approaches you have mentioned have specific usecases, where each of them dominates. Let's take one by one. (And a critic at the end)

Asynchronous app with callbacks

Although callbacks are treated old and not-trendy, they give us the flexibility of what I may call as "multiple return values".

getUserByName(name, callback) {
    // Caller can check the `err` and `status` to decide what to do next. 
    return callback(err, status, user);
}
  • Here, throwing errors will not work as expected. That is, you can't catch thrown errors with a try-catch. You can of course "return" a custom error. But that seems pointless, having the ability to return the "status" separately.
  • Returning a null object or RORO does not make sense with the additional status parameter. You might prefer to get rid of the status in favour of a null object or RORO though.

Asynchronous app with promises/async-await

Here, throwing errors will work. If you are using async-await, the catch of try-catch will get the error. Or if you are using promises as the are, the catch of promise chain will get it.

Synchronous app

Throwing errors and using try-catch works as expected. However, this will be a rare case in a real world application which uses any 3rd party service.

However, there is an issue with using exceptions after all. Whether you want to treat the "no-data" scenario as an error or not depends on the business logic.

  1. If you want to just tell the end user, that there is no such user, then using an exception will make sense.
  2. If you want to proceed to create a NEW user, then it might not make sense. You will need to differentiate explicitly whether the error was due to "no-data", or due to a "driver error", "timeout" etc. From my experience, using exceptions like this introduces bugs quite easily.

Thus the solution for that problem will be to use a Custom Error. Then again, there will be too many boiler plate code in the callee and the caller. And this might even cause confusion in the semantic meaning of the logic. On the other hand, too much usage of custom errors can cause inconsistency issues and may lead to a hard-to-maintain app over time. (This totally depends on how the app evolves. For instance, if the devs change frequently, different people will have different opinions on what these custom errors should be)


Opinionated answer

Having seen good applications and highly unmaintainable monoliths, I will give you my personal preference. But keep in mind that this may not be the ultimate solution.

Note: This (opinionated) solution is based on facts such as,

  1. Amount of boilerplate code required
  2. Semantic meaning of the code
  3. Easy-ness for a new developer to start working on the app
  4. Maintainability

Don't use Errors to handle the no-data scenarios. instead, check explicitly for "empty" data and handle the case separately. I will the RORO pattern here, with a slight difference. I do return an object, but receives parameters instead of an object. This depends on the number of parameters. If the number of parameters is too high, then it might make sense to receive an object. But In my opinion, that may be an indication of a bad business/architecture decision.

Consider the following example, of a user signin.

  1. If an error occurs while signing-in, I show the "erroneous" state to the end user.
  2. If the user was fetched successfully, I log the user in
  3. If the user is not there, I create a new user. (assuming I have enough data from the request it self to do so)

A pseudo code for that may look like this,

// Controller code
async signIn(name, email) {
  try {
    let user = await userDao.getUserByUserName(name);
    if (!user.exists) { 
       user = await userDao.createUser(name, email);
    }
    return user; 
  } catch(e) {
    // handle error
  }
}

or like this,

    // Controller code
async signIn(name, email) {
  try {
    let result = await userDao.getUserByUserName(name);
    if (!result.user) { 
       result = await userDao.createUser(name, email);
    }
    return result.user; 
  } catch(e) {
    // handle error
  }
}

A good case for the use of custom errors would be to differentiate whether the error occurred while retrieving the user or while creating the user.

The main benefit of using the status(either as a param from a callback, or as a property of a return object), is that you can use it to design the rest of the application as a state-machine(no-data scenario being only one such state. There can be many states in a complex business, based on user types, partial retrievals etc).

Sign up to request clarification or add additional context in comments.

Comments

1

Personally, I would recommend #1. There are a few reasons:

  1. It mirrors the behavior of other primitives in (accessing elements of arrays/hashes, result of find, etc), so other Javascript developers will be familiar with that pattern.
  2. The response itself is inherently falsey, which makes it trivial to check for the 'not found' case very succinctly. In this case, since you expect an object (and not a numeric, where 0 is falsey), this is a nice pattern.
  3. Null object pattern is more cumbersome. Users have to know how to check the object for the 'not found' case anyways, and that check will be very specific to how you implement the null object. It would be better to use a language primitive to articulate that nothing exists.
  4. I don't consider RORO significantly different than Null objects. The user still has to know how to inspect the response, and RORO implies that the return value is not the usable thing by itself...instead, the actual response they are looking for is embedded somewhere in the response.
  5. Exceptions should be used for truly "exceptional" cases. It is entirely reasonable for a list not to contain what you search for, so that is not really an exception. Additionally, exceptions require more code to handle. They are good for certain things, but this really isn't one of them.

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.