If-else and try-catch as functional constructs

In Practical guide to writing more functional Javascript, we walked through how to reason about our code in functional programming terms. In this guide, we will talk about a few utilities I like to use to reason about these concepts and help us navigate through the imperative constructs JavaScript natively provides.

Tread Lightly

I think that making imperative constructs in the language (if-else/try-catch) more declarative will improve the readability and testability of your code. It’s a strong opinion loosely held because I can sympathise with the hidden cost of it as well.

Right abstractions are hard because it forces you and your team to come to a consensus. And agreeing is hard — especially when you’re trying to rewrite simple language constructs as functional abstractions. It’s doubly hard because what code is more readable and what level to unit test things are very subjective as well.

Functional construct #1: conditionally

I argued for the need to have functional constructs in my earlier article. Let’s consider a basic language construct: if-else. What if we can express if-else as a functional construct?

The implementation goes like:

export const conditionally = (config) => (props) => {
  return config.if(props) ? 
    config.then(props) : config.else(props);
};

In straightforward terms, conditionally asks the question: can I write if-else in a way that in the truthy condition or false condition always returns a value by evaluating a function? Or in other words, if I could express what if-else does in a pure function, how does it look like?

It takes a config, that has three functions: if(), then() and else(). And it constructs a function which can receive your props argument.

When if(props) evaluates to true, it fires the then(props) or else, else(props). All three functions receive the same input and conventionally produce the same type of result.

If you use Typescript, we can enforce the input type and the result with generics. If the following looks complicated, or you don’t have experience with generics in Typescript, feel free to skip over the following example.

export const conditionally = <Props, Result>(options: {
  if: (props: Props) => any;
  then: (props: Props) => Result | Result;
  else: (props: Props) => Result | Result;
}) => (props: Props) => {
  return options.if(props) ? options.then(props) : options.else(props);
};

Let’s consider a normal if-else condition.

function getCarConfig(car) {
  let description;
  let newPrice;

  if (car.rating > 4) {
    description = "good car";
    newPrice = car.price + 1000 * car.rating;
  } else {
    description = "bad car";
    newPrice = car.price;
  }
  
  return {
    description,
    newPrice,
  }
}

The above example is an almost perfectly good way of writing this. But we can do better. Now let’s consider writing this with conditionally.

const hasGoodRating = rating => rating > 4;

const priceChange = conditionally({
  if: hasGoodRating,
  then: rating => 1000 * rating,
  else: () => 1000,
});

const getDescription = conditionally({
  if: hasGoodRating,
  then: () => "good car",
  else: () => "bad car",
});

function getCarCofig (car) {
  return {
    newPrice: priceChange(car.rating) + car.price,
    description: getDescription(car.rating)
  }
}

This might seem a bit verbose. But let’s analyse it a bit…

The different concerns are now handled by two different functions. conditionally has gently forced you to separate your concerns. This, in turn, gives you the option to test all these concerns in isolation, and conditionally mock them — adhering to most of the F.I.R.S.T principles of unit testing.

When someone else reads your code to understand what getCarConfig does, they don’t need to go to the implementation details of priceChange and getDescription, because you’ve named things properly. Your extractions now have a single responsibility and proper naming creates the least astonishment for a reader.

That IMHO is why I advocate embracing FP in Javascript. It forces you to break the problem into small atomic parts called functions. These functions:

  1. Separate your concerns
  2. Improves testability
  3. Naturally adheres to the Single Responsibility Principle
  4. With a bit of practice in naming things, the Principle of Least Astonishment is preserved

Functional construct #2: tryCatch

Exceptions are a powerful tool in a lot of languages. They provide a refuge from the unknown, unreasonable and unsafe boundaries of a system.

In Javascript, you can use try-catch:

function setUserLanguageCode(selectedLanguage) {
  const languageCode = getLanguageCode(selectedLanguage);
  
  let storedSuccessfully;
  
  try {
    window.localStorage.setItem("LANG_CODE", languageCode);
    storedSuccessfully = true;
  } catch (e) {
    storedSuccessfully = false;
  }
  
  return {
    storedSuccessfully
  }
}

But try-catch is a bit verbose. If you want to record the state (like storedSuccessfully there), you have to declare a let which signals a possible mutation of state, as with the example. Also semantically, try-catch signals a break in control flow and makes the code harder to read.

Let’s try to create a functional utility to mitigate some of those issues.

export function tryCatch({
  tryer,
  catcher
}) {
  return (props) => {
    try {
      return tryer(props);
    } catch (e) {
      return catcher(props, e.message);
    }
  };
}

Here, we encapsulate the try-catch construct in a function. tryCatch() will receive a config object with two functions. It then returns a function which will accept a single props object.

  1. tryer(props) will be evaluated, and return the result.
  2. While doing tryer(props), if an exception occurs, catcher(props) will be called.

Again, with Typescript, you can use generics to enforce the input types and the output types of this construct. If the generics here look a bit daunting, I’ve written a beginner’s intro to generics and why you should use them here.

export function tryCatch<Props, Result>({
  tryer,
  catcher
}: {
  tryer: (props: Props) => Result;
  catcher: (props: Props, message: string) => Result;
}) {
  return (props: Props) => {
    try {
      return tryer(props);
    } catch (e) {
      return catcher(props, e.message);
    }
  };
}

With that in mind, let’ try to refactor our earlier example.

const storeLanguageCode = tryCatch({
  tryer: (languageCode) => {
    window.localStorage.setItem("LANG_CODE", languageCode);
    return true;
  },
  catcher: (languageCode, errorMessage) => {
    logger.log(`${errorMessage} <-- happened while storing ${languageCode}`);
    return false;
  }
});

const setUserLanguageCode = pipe(
  getLanguageCode,
  languageCode => storeLanguageCode(langaugeCode), // or just storeLanguageCode
  storedSuccessfully => ({ storedSuccessfully })
);

// setUserLanguageCode("en-US") will work as before.

If you’re unfamiliar to the usage of pipe, check out my earlier article on a practical guide to writing functional javascript. TLDR is that it’s a reverse _compose()_.

Again we can see that our functional construct has forced us to break out the unsafe part of our code into a different function. Furthermore, we have ended up with 3 discrete functions that we can pipe() together to get our end result.

The benefits I explained earlier apply here as well. Of which the most important is readability. Now when someone reads your setUserLangage function, they don’t have to take the cognitive burden of parsing the try-catch upfront, because that is encapsulated in an aptly named storeLanguageCode function.

Closing notes

I don’t advocate writing things in conditionally and tryCatch just for the sake of doing so. Sometimes, a simple ternary operation or a vanilla if-else keeps things perfectly readable. But, I personally try to follow a convention as much as I can. Conventions allow developers to make fewer decisions and conserve brain power.

And conditionally and tryCatch makes a lot of good decisions for me by default.

Small functions considered harmful lists the opposite view to this approach. I don’t fully agree with some of the things in that article, and some of it becomes just doesn’t hold any water in an FP paradigm. Nevertheless, I implore you to go and read it.

There are no absolutes in software engineering. No, not even DRY. As always, keep exploring and use your best judgement.