Control flow for invalid actions – Conditionals, try/catch and Booleans

An important part of clean code is handling control flow properly.

In this article, we’ll examine control flow for actions that might be invalid or may fail. For example, in a video game, a player might try to purchase something without having enough money. In these cases, you need to have something like a conditional check. If you don’t the code may crash when it tries to spend money that player doesn’t have.

There are a few options for handling these types of actions. These are to:

  • tell, don’t ask
  • look before you leap
  • use try / catch
  • return a Boolean

Here is each option in more detail:

Tell, don’t ask

"Tell, don’t ask" is a guideline that helps you write better code. It helps you structure code so that it’s easy to work with. As a side benefit, it also reduces the number of conditionals you need.

Normally, if you don’t use "tell, don’t ask", some code will be like this:

  1. obtain an object from somewhere
  2. ask the object for some data
  3. use a condition to see what you can do with that data
  4. do something with that data or with the object

Here’s a code example:

function calculateArea(shape) { // obtain shape object
  const type = shape.type; // ask object for some data
  if (type === 'circle') { // have a condition to see what you can do
    return Math.PI * shape.radius ** 2; // do something
  } else if (type === 'rectangle') {
    return shape.length * shape.width;
  }
}

class Circle {
  constructor(radius) {
    this.radius = radius;
    this.type = 'circle';
  }
}

class Rectangle {
  constructor(length, width) {
    this.length = length;
    this.width = width;
    this.type = 'rectangle';
  }
}

Instead, with "tell, don’t ask", the code is like this:

  1. get an object
  2. do something with it

To achieve this, you use polymorphism. You put the logic inside the object. Then, you tell the object what to do. You don’t have to ask it what it’s capable of doing first.

Here’s a code example:

function calculateArea(shape) {
  // no need to query object, just tell it to do something
  return shape.calculateArea();
}

class Circle {
  constructor(radius) {
    this.radius = radius;
  }
  // logic to calculate area is inside the object
  calculateArea() {
    return Math.PI * this.radius ** 2;
  }
}

class Square {
  constructor(length, width) {
    this.length = length;
    this.width = width;
  }
  // logic to calculate area is inside the object
  calculateArea() {
    return this.length * this.width;
  }
}

In the code above, the classes Circle and Square both have a method for calculating the area. Then, in the standalone function, you just call the method. You don’t need conditionals to check what type the shape is.

This is an application of the principle of separation of concerns. Separation of concerns talks about organising your code into sensible units that are easy to work with. For more information about it, please see Clean code and programming principles – The ultimate beginner’s guide.

However, "tell, don’t ask" isn’t always possible. You can’t just keep piling logic inside objects. Sometimes, you’ll need to use different control flow mechanisms.

For example, you might have two different systems that work together. In this case, you can’t necessarily move logic from one system to the other. You’ll have to use something like if / else instead.

Here’s a code example:

function handleNewTowerRequest() {
  if (moneySystem.balance < 500) {
    // tell player they don't have enough money
  } else {
    moneySystem.spend(500);
    defenceSystem.buildDefenceTower();
  }
}

In the code above, you can’t necessarily put the line moneySystem.spend(500) inside the defenceSystem code. You may want to keep the two systems separate. In this case, you’ll use if / else and let some other code handle the interaction between the two systems.

Conditional checks and try / catch

Another approach is to check whether something is valid before attempting it. This is also known as "look before you leap" (LBYL). Or, the direct alternative is to use try / catch. This is also known as "easier to ask for forgiveness rather than permission" (EAFP).

Conditionals (look before you leap)

The format for checking with conditionals is something like:

  1. if (some condition to check if something is possible)
  2. then: do something
  3. otherwise: do something else

Some example code is:

function handleNewTowerRequest() {
  if (moneySystem.balance < 500) {
    // tell player they don't have enough money
  } else {
    moneySystem.reduce(500);
    defenceSystem.buildDefenceTower();
  }
}

Here’s another example:

function handleUserFormSubmission(userData) {
  if (userData.name === '') {
    // tell user that the "name" is required
  } else if (userData.email === '') {
    // tell user that the "email" is required
  } else {
    registerUser(userData);
  }
}

.NET recommends this option when possible instead of try / catch.

Try / catch (easier to ask for forgiveness rather than permission)

With this option, you attempt to do something in a try block, without checking if it’s possible. If it’s possible, it will succeed. Otherwise, it will throw an exception and execution will move to the catch block.

For example:

const user = {
  name: 'Bob',
  age: null,
};

function handleUserEvent() {
  try {
    user.age *= 2;
  } catch (error) {
    // notify user that they haven't submitted their age, so their age can't be doubled
  }
}

This option only seems to be used when the code in the try block is the common case.

This option is the convention in Python.

Conditionals vs try / catch

Realistically, choosing one over the other is a stylistic convention. They both produce similar code except for some superficial differences.

When deciding which one to use, I recommend considering the convention in your programming language. Sticking to conventions is useful.

Otherwise, for more information on the pros and cons between them, please see LBYL vs EAFP.

Return a Boolean

The last option is to attempt to do something without checking. Then, return a Boolean value that signifies whether it succeeded or not.

For example:

function buildTower() {
  if (!player.balance < 500) {
    return false;
  } else {
    player.balance -= 500;
    // do some stuff to build the tower
    return true;
  }
}

function main() {
  const wasSuccessful = buildTower();
  if (!wasSuccessful) {
    // notify the user that they don't have enough money
  } else {
    // continue normal execution
  }
}

This option is… acceptable… However, it has multiple disadvantages. It:

Recommendations

My personal recommendations, in order of priority, are to:

  1. consider whether the code structure can be improved to remove the need for conditions. For example, use "tell, don’t ask".
  2. pick either "look before you leap" or "easier to ask for forgiveness rather than permission"
  3. return a Boolean

Final notes

So that’s it for this article. I hope that you found it useful.

As always, if any points were missed, or if you disagree with anything, or have any comments or feedback then please leave a comment below.

For the next steps, I recommend looking at the other articles on clean code.

Alright, thanks and see you next time.

Credits

Image credits:

  • Train tracks – Photo by Nubia Navarro (nubikini) from Pexels
  • Legos – Photo by Daniel Cheung on Unsplash
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments