Performance Zone is brought to you in partnership with:

Open-source software enthusiast, Node.js and PHP expert, Lean & Agile management fan, Leadership & motivation experimenter, CEO at marmelab Francois has posted 12 posts at DZone. You can read more from them at their website. View Full User Profile

Node.js for PHP Programmers #3: Exceptions and Errors

01.19.2013
| 27998 views |
  • submit to reddit

Just like PHP, JavaScript supports Exceptions - only they're called Errors. However, due to the asynchronous nature of Node.js, the classic try/catch strategy doesn't work. To catch asynchronous errors, Node strongly encourages a particular signature for callback functions. I'll explain all that shortly, but now I have a confession to make.

Digest3

Sometimes Things Go Wrong

I don't eat meat. It's not because I like animals too much - I don't believe chickens have a soul. It's just that I ate a lot of meat previously in my life, and if every person on this planet eats as much meat as I did, the place will become awful to live in. If you don't believe me, go watch this video on YouTube and thank Prof. Albert Bartlett for the depression. So I quit eating flesh a year ago, and so far my teeth haven't fallen or something.

But to be honest, I still eat meat every once in a while. You see, vegetarians have to take pills to compensate for the nutriments they don't get from the meat. I prefer to eat meat than pills. And since the Sunday meal is often a family moment, and since meat MUST be on the menu, I eat meat almost every Sunday.

And I like meat. That means that I love the Sunday meals, whether it's veal sauté with white wine, or roasted chicken with French fries. And last time I ate too much of it. Then things started going wrong.

The PHP Digestive System

I won't expose in details the miserable Sunday afternoon I spent after that. However, since the audience of this blog is mostly composed of programmers, I can write what happened in PHP.

<?php
class HumanBody
{
  const DIGESTIVE_CAPACITY = 10;
  protected $totalNutriments = 0;

  public function eat($meal)
  {
    try {
      foreach ($meal as $element) {
        $this->digest($element);
      }
      return true;
    } catch (Exception $e) {
      $this->totalNutriments = 0;
      $this->vomit($meal);
      return false;
    }
  }

  public function digest($element)
  {
    // actual digestion taking place here
    // transforming $element into $nutriments
    $this->totalNutriments += $nutriments;
    if ($this->totalNutriments > self::DIGESTIVE_CAPACITY) {
      throw new Exception('Cannot digest: You ate too much');
    }
    return $nutriments;
  }

  public function vomit($meal)
  {
    // you get the idea
  }
}

The Node.js Digestive System

JavaScript is another good way to express my misery. And since Node.js puts a dose of asynchronicity on top of JavaScript, it's a good opportunity to show that digestion can take time. Here is the equivalent of the previous class, written for Node.js:

function HumanBody() {
  this.digestiveCapacity = 10;
  this.totalNutriments = 0;
};

HumanBody.prototype.eat = function(meal, callback) {
  var body = this;
  var processedElements = 0;
  try {
    meal.forEach(function(element) {
      body.digest(element, function() {
        processedElements++;
        if (processedElements == meal.length) {
          callback(true);
        }
      });
    });
  } catch (err) {
    this.totalNutriments = 0;
    this.vomit(meal);
    callback(false);
  }
}

HumanBody.prototype.digest = function(element, callback) {
  setTimeout(function() {
    // actual asynchronous digestion taking place here
    // transforming element into nutriments
    this.totalNutriments += nutriments;
    if (this.totalNutriments > this.DIGESTIVE_CAPACITY) {
      throw 'Cannot digest: You ate too much';
    }
    callback(nutriments);
  }, 100);
}

HumanBody.prototype.vomit = function(meal) {
  // you get the idea
}

In digest(), setTimeout() is there to simulate an asynchronous processing. Asynchronous functions don't use the return statement to return their result. Instead, they call the given callback with the result as parameter. That's why digest() ends with callback(nutriments) instead of return $nutriments as in PHP. Therefore, eat() has to provide a callback when calling digest(). When all the elements are processed, eat() can return true by calling its own callback.

Tip: If you don't understand why the line var body = this is necessary, you've not yet encountered the First Gotcha of JavaScript. Head to my article on "this" for more details.

Second Tip: The test if (processedElements == meal.length) is a way to determine when the asynchronous meal digestion is complete. The need to resynchronize asynchronous processing arises quite often in Node.js, and instead of using boilerplate code similar to this one, you should definitely use the async module. It offers resynchronization, and many other things. In my opinion, it should be part of the Node.js core.

Wait, It Doesn't Work

The digest() function is asynchronous, remember? So when Node executes this piece of code from eat():

try {
  meal.forEach(function(element) {
    body.digest(element, function() {
      processedElements++;
      if (processedElements == meal.length) {
        callback(true);
      }
    });
  });
} catch (err) {
  this.totalNutriments = 0;
  this.vomit(meal);
  callback(false);
}

Node iterates over every element in the meal, executes HumanBody.digest() on all of them, then ends. That means that if a problem of digestive capacity occurs while in digest(), it will be after the end of the eat() execution, and that means after the try {} catch {} statement is finished. The Error will never get caught, and this is dramatic. Imagine a human body unable to vomit even when filled up over the limit? Explosion!

No try/catch in Node.js

In asynchronous programming, the communication channel between the caller function and the callee is the callback. The only solution to overcome the uncaught error is to pass the Error to the callback. So the eat() and digest() functions must be refactored as follows:

function HumanBody() {
  this.digestiveCapacity = 10;
  this.totalNutriments = 0;
};

HumanBody.prototype.eat = function(meal, callback) {
  var body = this;
  var processedElements = 0;
  meal.forEach(function(element) {
    body.digest(element, function(error) {
      if (error) {
        this.totalNutriments = 0;
        this.vomit();
        return callback(false);
      }
      processedElements++;
      if (processedElements == meal.length) {
        callback(true);
      }
    });
  });
}

HumanBody.prototype.digest = function(element, callback) {
  setTimeout(function() {
    // actual asynchronous digestion taking place here
    // transforming element into nutriments
    this.totalNutriments += nutriments;
    if (this.totalNutriments > this.DIGESTIVE_CAPACITY) {
      callback(new Error('Cannot digest: You ate too much'), null);
    }
    callback(null, nutriments);
  }, 100);
}

HumanBody.prototype.vomit = function(meal) {
  // you get the idea
}

The important thing is is the new signature of the callback passed to digest():

function(error) {
  if (error) {
    // ...
  }
  // ...
}

The callback expects that an error may be passed by the asynchronous digestion. And it's the responsibility of the callback to deal with this error if it exists, and to proceed with normal operations otherwise.

And it's the only way to catch an error thrown in an asynchronous function. So forget about try {} catch {} in Node.js. You will use them very seldom, while callbacks expecting errors are everywhere.

Always Carry Your Errors With You

What happens with the asynchronous digestion can happen in every asynchronous function in Node.js. Thats's why the core asynchronous functions always show err as the first argument of every callback. If the operation was completed successfully, then the first argument will be null or undefined. Here is an example with the equivalent of PHP's realpath():

require('fs');
fs.realpath(path, function(err, resolvedPath) {
  if (err) {
    // ..
  }
  // use the resolvedPath
});

When you design your own asynchronous functions, follow this rule of thumb and always put the error as first argument of the callback. It's also a good way to remember that you have to deal with the error cases first.

Conclusion

There is still a flaw in the Node.js HumanBody implementation. If something bad happens in the middle of the meal digestion, the vomiting is triggered and the main callback is called with false as argument. But the forEach() loop continues. That means that the callback may be called a second time with true as argument. The correction is left as an exercise for the reader.

As for my digestive problems, they're over. But "Once bitten, twice shy", so I always think that something bad may happen to me when eating meat, and I'm very careful.

If you don't want to give up meat, you should be careful, too. Always expect that something bad may happen, and you will design robust and effective programs - even in asynchronous Node.js.

Published at DZone with permission of its author, Francois Zaninotto. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)