Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript: Using pattern Null object pattern, RORO, undefined etc. as returning parameter for a Get-function in case requested object „not found“

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?

like image 612
Mani Avatar asked Aug 06 '26 09:08

Mani


1 Answers

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).

like image 190
Pubudu Dodangoda Avatar answered Aug 08 '26 00:08

Pubudu Dodangoda



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!