Cuando queremos trabajar con tareas asíncronas en nodeJs básicamente tenemos tres forma de conseguirlo, la primera y más importante son los callbacks ya que casi todo en nodejs esta construido a partir de este, el segundo es utilizar promises que podemos decir es un callback muy optimizado con estados, y finalmente tenemos los async/await que nos permiten tener un código mucho más limpio al crear y consumir promesas.

Callback vs Promise vs async/await

Repositorio

Cuando queremos trabajar con tareas asíncronas en nodeJs básicamente tenemos tres forma de conseguirlo, la primera y más importante son los callbacks ya que casi todo en nodejs esta construido a partir de este, el segundo es utilizar promises que podemos decir es un callback muy optimizado con estados, y finalmente tenemos los async/await que nos permiten tener un código mucho más limpio al crear y consumir promesas.

Callbacks

Un callback es una función en espera de ser ejecutada en un orden y para ser más cercanos a la definición es una función que toma como parámetro otra función, esto significa que necesitamos que se ejecute primero una tarea y cuando este listo el resultado de esta se le podrá pasar a la siguiente tarea. Tenemos un concepto muy util y poderoso, sin embargo también puede convertirse en un desastre cuando empiezan a concatenarse muchos callbacks (callback hell), por esto se debe utilizar con mucho cuidado.

En este primer ejemplo tenemos una función que espera se escriba una cadena en la terminal y pasa ese resultado a un callback, el cual es una función que imprime el resultado en consola.

const readline = require('readline');

function procesarEntradaUsuario(callback) {
    // lee de la entrada estándar (teclado) y escribe en la salida estándar
    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });
    // la función se retarda hasta que se escriba el nombre
    rl.question('Por favor ingresa tu nombre: ', (nombre) => {
        // saludar(name)
        callback(nombre);
        rl.close();
    });
}

function saludar(nombre) {
    console.log(`Hola ${nombre}`);
}


procesarEntradaUsuario(saludar);

Un callback es una función a la cual podemos pasar cuantos parámetros necesitemos, pero la convención es tener como mínimo un error y un success. En el siguiente ejemplo estamos simulando el método de readFile del modulo fileSystem, el cual lanza un error si se genera un numero impar y una respuesta success con un numero par.

function imprimirArchivo(error, success) {
    if (error) {
        console.log(`Se genero un error porque ${error} es impar`);
        return;
    }
    console.log(`Tenemos una respuesta exitosa ${success} par`);
}

function leerArchivo(path, callback) {
    setTimeout(() => {
        const number = Math.floor(Math.random() * 10);
        console.log("buscando en un archivo un numero y es un proceso asíncrono");
        if (number % 2 !== 0) {
            // la convención en node es tener dos parámetros uno para el error y otro para la respuesta
            callback(number, null);
            return;
        }
        callback(null, number);
    }, 1000);
}

leerArchivo('mi_archivo', imprimirArchivo)

Promises

Una promise es un objeto que sirve como proxy, donde existen estados para poder manejar las respuestas o los fallos, esto quiere decir que sabemos cuando una tarea esta pendiente, fue ejecutada o rechazada. Una promesa recibe un callback que tiene un reject y un resolve e internamente después de todo un proceso optimizado se resuelve exitosamente o se rechaza.

En este primer ejemplo tenemos una función que retorna una promesa (un objeto) y para verificar el resultado debemos utilizar el método then.

const readline = require('readline');

function procesarEntradaUsuario() {
    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });

    return new Promise((resolve, reject) => {
        // la función se retarda hasta que se escriba el nombre
        rl.question('Por favor ingresa tu nombre: ', (nombre) => {
            resolve(nombre);
            rl.close();
        });
    })
}

function saludar(nombre) {
    console.log(`Hola ${nombre}`);
}

procesarEntradaUsuario()
 .then(success => {
    return saludar(success);
  });

En este ejemplo creamos una promesa que resuelve o rechaza si es un numero par o impar, para verificar si existió un error debemos usar el método catch.

function leerArchivo(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const number = Math.floor(Math.random() * 10);
            console.log(`buscando en el archivo ${path} un numero y es un proceso asincrono`);
            if (number % 2 !== 0) {
                reject(number);
                return;
            }
            resolve(number);
        }, 1000);
    })

}

leerArchivo('mi_archivo').then(success => {
    console.log(`Tenemos una respuesta exitosa ${success} par`);
}).catch( error => {
    console.log(`Se genero un error porque ${error} es impar`);
});

En este ejemplo podemos ver una de las principales ventajas de una promesa, podemos concatenar promesas y el código no va a ser tan difícil de leer como seria con los callbacks.

// promesa 1
const suma = function(total, numero){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(total + numero)
        }, 1000);
    });
}
// promesa 2
const resta = function(total, numero){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(total - numero)
        }, 1500);
    });
}
// promesa 3
const multi = function(total, numero){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(total * numero)
        }, 100);
    });
}


const total = 10;

suma(total, 2).then(resSuma => {
    // resSuma = 10 + 2
    return resta(resSuma, 5);
}).then(resResta => {
    // resResta = 12 - 5
    return multi(resResta, 3);
}).then(resMulti => {
    // resMulti = 7 * 3
    console.log(resMulti);
}).catch(error => {
    console.log(error);
})

Async/await

El async/await se creo para simplificar el comportamiento de las promesas, primero debemos tener una función con la palabra reservada async esto hace que lo que sea que tengamos en ella se convierta en una promesa y tenemos la palabra reservada await que se encarga de pausar la ejecución hasta que se resuelva una promesa.

En el siguiente ejemplo se muestra como consumir una promesa con await y como convertimos la función saludar en una promesa con async.

const readline = require('readline');


async function procesarEntradaUsuario() {
    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });

    return new Promise((resolve, reject) => {
        // la función se retarda hasta que se escriba el nombre
        rl.question('Por favor ingresa tu nombre: ', (nombre) => {
            resolve(nombre);
            rl.close();
        });
    })

}

// se convierte en una promesa
async function saludar() {
    // espero hasta resolver la promesa
    const nombre = await procesarEntradaUsuario();
    return `Hola ${nombre}`;
}


saludar().then( sucess => {
    console.log(sucess);
});

Los siguientes ejemplos, como han podido notar son los mismos que hemos utilizado durante toda la lectura, esto significa que tenemos un mismo código escrito con los tres conceptos que queremos abordar para tener un punto de comparación.

function leerArchivo(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const number = Math.floor(Math.random() * 10);
            console.log(`buscando en el archivo ${path} un numero y es un proceso asincrono`);
            if (number % 2 !== 0) {
                reject(number);
                return;
            }
            resolve(number);
        }, 1000);
    })

}

async function callAsync() {
    try {
        const number = await leerArchivo('mi_archivo');
        return `Tenemos una respuesta exitosa ${number} par`;
    } catch (error) {
        return `Se genero un error porque ${error} es impar`;
    }

}

callAsync().then(success => {
    console.log(success);
}).catch(error => {
    console.log(error);
});
const suma = function (total, numero) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(total + numero)
        }, 1000);
    });
}

const resta = function (total, numero) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(total - numero)
        }, 1500);
    });
}

const multi = function (total, numero) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(total * numero)
        }, 100);
    });
}

async function callAsync() {
    const total = 10;
    try {
        const resSuma = await suma(total, 2);
        const resResta = await resta(resSuma, 5);
        const resMulti = await multi(resResta, 2);
        return resMulti;
    } catch (error) {
        return error;
    }
}

callAsync().then(success => {
    console.log(success);
}).catch(error => {
    console.log(error);
});