NodeJS y el Callback Hell

Uno de los primeros desafíos con los que me encontré al comenzar a programar con NodeJS es el conocido callbackhell resultante muchas veces a causa de la naturaleza asíncrona que tantas ventajas provee. Existen numerosos sitios que enseñan a cómo tratar con esto a los cuales me quiero sumar dando a conocer las formas que mejor resultado me han dado.

Usaremos a modo de ejemplo uno de los casos más comunes con los que me encontré al programar en SailsJS donde muchas veces se requiere el resultado de funciones asíncronas previas para seguir procesando los resultados. Al enfrentarse a este tipo de escenarios no es difícil terminar provocando un algoritmo del tipo:


    someAction: function(req, res) {
      Model1.action({ someCondition }).exec(function(err, res) {
        if(err){
          // manejo del error
        }
        // algo de lógica por aquí que involucra a res
        Model2.action({ someCondition2 }).exec(function(err, res2) {
          if(err){
            // manejo del error
          }
          // más lógica por aquí con res2
          Model3.action({ someCondition3 }).exec(function(err, res3) {
            if(err){
              // manejo del error
            }
            // algo más de lógica por acá con res3 y también con res2
            // y terminamos
          }
        }
      }
    }
Creación de funciones ad-hoc

Creando funciones específicas para cada caso se aumenta la claridad del algoritmo generado, y si usamos algo de ingenio podemos diseñar las funciones de modo que puedan ser reusadas posteriormente. El ejemplo anterior utilizando este método quedaría de esta forma:


    someAction: function(req, res) {
      Model1.action({ someCondition }).exec(step2);
    }

    function step2(err, res) {
      if(err) {
        // manejo de error
      }
      Model2.action({ someCondition2 }).exec(step3);
    }

    function step3(err, res2) {
      Model3.action({ someCondition3 }).exec(function(err, res3) {
        // aquí jugamos con res2 y res3
      }
    }

La claridad aumenta utilizando este enfoque, pero aún se tendrían que crear en cada una de las subfunciones un manejo adecuado de errores lo cual dista de ser óptimo. Además usando esta forma solo se pueden manejar rutinas del tipo fire and forget, donde la función originadora del primer llamado no requiere los resultados de las posteriores, sino que solo que se hagan.

Para poder utilizar los resultados de las funciones internas utilizando un enfoque similar se puede recurrir a hacer una función que envuelva a las posteriores, y que al terminar ejecute un callback de la siguiente manera:


    someAction: function(req, res) {
      doTheStuff(function(err, results) {
        if(err) {
          // manejo de errores
        }
        // jugamos con results << contiene res, res2 y res3
      });
    }

    function doTheStuff(cb){
      Model1.action({ someCondition }).exec(function(err, res) {
        if(err) return cb(err);
        step2and3(cb, {res: res})
      });
    }

    function step2and3(cb, results) {
      Model2.action({ someCondition2 }).exec(function(err, res2) {
        if(err) return cb(err);
        Model3.action({ someCondition3 }).exec(function(err, res3) {
            results.res2 = res2;
            results.res3 = res3;
            cb(err, results);
        }
      }
    }
Usando esta forma se gana acceso a todos los resultados parciales en el callback de la primera función ejecutada. Además el manejo de los errores se hace en un solo lugar y se rompe la cadena de ejecución de funciones al encontrar el primer error. Aquí asumimos que los pasos 2 y 3 se pueden englobar en uno solo, para demostrar que añadiendo un nivel más no se pierde claridad al posponer el manejo de los resultados hacia un mismo callback. Ahora pasaremos a la otra forma, que no es tan casera y ayuda bastante con la claridad... ###### El módulo Async! Con async se puede evitar mucho anidamiento de funciones al proveer diferentes métodos para programar que facilitan el paralelismo. El catálogo completo lo pueden revisar siguiendo este [link](https://github.com/caolan/async). Yo les mostraré mi función favorita de las ahí proveídas: **async.auto** El signature de esta función es como se muestra a continuación:

    async.auto({
      one: function(cb) {
        // lógica por aquí. Terminamos llamando a cb()
      },
      two: function(cb) {
        // más lógica por aquí. Al terminar llamamos a cb()
      },
      three: [‘one’, ‘two’, function(cb, results) {
        // aquí contamos con results.one y results.two para jugar
      }]
    }, function(err, results) {
      // chequeo de errores en un solo lugar
      // results.one, results.two, results.three...
    });

En este caso las funciones one y two correrán en paralelo. Al terminar ambas comenzará a ejecutarse three. Esto se debe a que al definir three también se incluyeron como prerrequisitos one y two(dentro del arreglo en la definición de three).

El uso de esta función provee un par de ventajas, como por ejemplo los ya nombrados paralelismo y la declaración de prerrequisitos!, además las funciones intermedias pueden acceder a los resultados de las funciones previas a través del segundo parámetro de la función(results en el ejemplo).

Otra gran ventaja es que el manejo de los errores puede ser definido en un solo lugar. Si cualquiera de las funciones dentro del primér parámetro de la función async.auto llama a su respectivo callback con un error, la ejecución de posteriores funciones se cancela y se pasa inmediatamente al callback final con el error.

Así el ejemplo inicial utilizando esta función quedaría…


    someAction: function(req, res) {
      async.auto({
        res: function(cb) {
          Model1.action({ someCondition }).exec(cb);
        },
        res2: function(cb) {
          Model2.action({ someCondition2 }).exec(cb);
        },
        res3: function(cb) {
          Model3.action({ someCondition3 }).exec(cb);
        }
      }, function(err, results) {
        if(err){
          // manejo de errores
        }
        // jugar con results << contiene res, res2 y res3
      });
    }

Para el ejemplo no era realmente necesario el uso de prerrequisitos, pero si quisieran usarlos ya saben cómo :D. Como pueden ver el resultado queda mucho más limpio y además es en menos líneas de código.