Ariel Rey

¿Callbacks?, llámame cuando termines

Hola 👋, soy Ari Rey. Tal vez me recuerdes por "el curso de NodeJS sin ejecutar una linea de código". En el capitulo de hoy vamos a ver qué es un callback.

alt

¿Qué es?

Un callback es una función que es enviada cómo argumento de otra función 🤔. Sí, eso es todo, tan solo 13 palabras definen lo que es un callback. Vamos a ejemplificar esto de la manera más simple.

En el siguiente ejemplo vamos a declarar una función llamada soyUnaFuncion.

function soyUnaFuncion() {
  // Esto es una función
}

Ahora, hagamos que esta función reciba un argumento.

function soyUnaFuncion(soyUnArgumento) {
  // Esto es una función que recibe un argumento
}

Hasta ahora no vemos nada del otro mundo 🌎, y si lo ven avisenme. Hagamos que la función haga algo.

function soyUnaFuncion(soyUnArgumento) {
  console.log('Estoy haciendo algo'); // Que gracioso...
}

¿El argumento llamado soyUnArgumento es un callback? Si y no 😅. Javascript es un lenguaje tipado dinámico, es decir que esta variable puede ser cualquier cosa hasta que le asigne un valor. Podríamos decir que este argumento se va a comportar como un callback cuando la misma se ejecute. Veamos el siguiente ejemplo.

function soyUnaFuncion(soyUnArgumento) {
  console.log('Estoy haciendo algo');

  // Ejecuto el argumento
  soyUnArgumento();
}

Recién en este momento podemos decir que el argumento soyUnArgumento de la función soyUnaFuncion es un callback 👏. Vamos a ponerle un nombre más representativo.

function soyUnaFuncion(soyUnCallback) {
  console.log('Estoy haciendo algo');

  // Ejecuto el callback
  soyUnCallback(); // Qué creatividad, ¿no?
}

Entonces, un callback es una función que es enviada cómo argumento de otra función y la misma, debe ser ejecutada, es decir, cumplir su propósito 🙄, sino sería un argumento más...

¿Para qué sirven?

Dependiendo el año en el que empezaste a programar, era el uso que se le daba. Antes de NodeJS 👨‍🦳, Javascript se utilizaba solamente dentro de los Navegadores para darle funcionalidad a las páginas Web (HTML). En ese entonces los callbacks se usaban para ejecutar funciones cuando determinados eventos ocurrían. Quizá hayas escuchado hablar de algunos de los siguientes eventos:

  • click
  • blur
  • focusin
  • focusout
  • keypress

Prehistoria

Estos eventos podían ser asignados a un elemento del HTML. Cuando se realizara este evento sobre el elemento, este ejecutaría una función (callback). Veamos el siguiente ejemplo:

function cuandoSeHagaClick() {
  console.log('Se hizo click');
}

// Buscar el elemento con el id "buscame"
const elemento = document.getElementById("buscame");

// Se le asigna una función a ejecutar cuando el evento ocurra
elemento.addEventListener("click", cuandoSeHagaClick);

En el ejemplo anterior estamos declarando una función llamada cuandoSeHagaClick y luego le estamos diciendo al elemento buscame que cuando se le haga click, ejecute la función cuandoSeHagaClick.

Esto era lo que hacíamos todos los días antes de NodeJS, React, Angular, Backbone, etc...

Edad Media

Avanzando en el tiempo ⏰, empezaron a aparecer los famosos AJAX, es decir, el uso del Objeto XMLHttpRequest. Si bien ya existían, no eran muy usados y la practica de ir al server para buscar información y luego reemplazar el contenido no era de lo más común. Esto se empezó a popularizar con el uso de una librería llamada JQuery.

Es aquí cuando los callbacks se volvieron aún más populares. Se empezaron a ejecutar llamadas desde el Navegador al servidor para filtrar resultados, pasar de páginas, etc...

Imaginemos que tenemos una página Web. En ella tenemos un listado de resultados y un filtro en donde podemos elegir un valor. Ni bien hagamos click en un elemento del HTML se van a filtrar los resultados. Veamos el código:

function reDibujaElListado(resultados) {
  // Lógica para borrar los resultados y mostrar los obtenidos por el servicio
}

function filtrarReultados() {
  // Obtenemos el dropdown con los filtros a elegir
  const filtro = document.getElementById("filtro");
  // Valor del dropdown seleccionado
  const valorAFiltrar = filtro.options[filtro.selectedIndex].value;

  // Consultamos al servidor y luego con los resultados llamamos a reDibujaElListado para que lo haga
  restclient.get(`servidor/filtrar/${valorAFiltrar}`, reDibujaElListado);
}

// Buscar el elemento con el id "elementoQueFiltra"
const elemento = document.getElementById("elementoQueFiltra");

// Se le asigna una función a ejecutar cuando el evento ocurra
elemento.addEventListener("click", filtrarReultados);

Cómo pudieron ver en el ejemplo anterior, la complejidad de este código se volvió exponencial. Tenemos un evento que se ejecuta cuando se hace click sobre un elemento, luego este ejecuta una llamada a un servidor que al terminar llama a otra para mostrar el resultado. Y todo esto suponiendo que salió todo bien, pero en caso de que algo salga mal, también deberíamos ocuparnos de mostrar él mismo en pantalla.

Edad Contemporánea

Con la llegada de NodeJS del lado del servidor, el boom 💣 del callback nació. NodeJS utiliza un modelo de comunicación no bloqueante (ver curso de NodeJS) para comunicarse. Es decir que cuando ejecuta algo que toma tiempo (que no es inmediato) lo hace en otro plano y mientras continua con la ejecución. Por ejemplo:

console.log('1');

setTimeout(function () {
  console.log('2');
}, 1000);

console.log('3');

El siguiente código imprime en la consola:

> 1
> 3
> 2

Esto es porque setTimeout se ejecuta luego de 1 segundo (1000 milisegundos). En un modelo bloqueante de ejecución se esperaria 1 segundo antes de ejecutar la siguiente linea de código.

Si quieren entender más sobre esto, pueden leer el curso de NodeJS que estuve escribiendo

Es por esto que los callbacks se volvieron extremadamente usados, ya que prácticamente todo lo que hacemos del lado del servidor lleva tiempo, como por ejemplo:

  • Levantar un servidor
  • Conectarse a una base de datos
  • Ejecutar una consulta en una base de datos
  • Consultar un servicio

A continuación voy a ejemplificarles cómo sería levantar un servidor y realizar una consulta a la base de datos. La consulta va a ser impresa una vez que el servidor se haya levantado. El siguiente código es ilustrativo.

const express = require('express');
const db = require('postgresql');

// Create Express application
const app = express();

app.listen(3000, function () {
  // Recién ahora esta el servidor levantado
  console.log('Server funcionando');

  // Me conecto a la base de datos
  db.connect('http://localhost:3333', {
    database: 'testingDB',
    user: 'test',
    password: 'test',
  }, function (conexion) {
    console.log('Me conecte a la base de datos');

    // Realizo la consulta a la base de datos
    conexion.consultar('select * from table', function (data) {
      console.log(`Resultado: ${data}`);
    });
  });
});

Me exploto la cabeza... 🤯 Como pueden ver prácticamente todo lo que ocurre del lado del servidor lleva tiempo ⏳ y necesitamos de un callback para avisarnos que lo que queríamos hacer ya termino. Por ejemplo:

  1. Levantar el servidor en el puerto 3000
  2. Conectarse a la base de datos
  3. Realizar la consulta

Manejo de Errores

En todos los ejemplos que estuvimos viendo, en ninguno manejamos los errores. Si en algún punto de la ejecución algo fallo, ya sea al crear el servidor, conectar a la base de datos o ejecutar la consulta a la base de datos, el código habría explotado por los aires 💥.

Podría decirse que existe una convención (nunca la vi escrita) que los callbacks siempre reciben 2 argumentos. Un primer argumento que representa un error y un segundo argumento que representa el resultado de la ejecución. En caso de que haya habido algún error, el primer argumento estará populado con una instancia del objeto Error, sino tendrá el valor de null. Veamos el siguiente ejemplo:

function miCallback (error, result) {
  // En caso de error imprimo un mensaje en consola y corto la ejecución del código mediante un return.
  if (error != null) {
    console.log(`Ha ocurrido un error: ${error.message}`);
    return;
  }

  // Imprimo el resultado en consola
  console.log(result);
}

// Consulto a una base de datos
db.consultar('select * from tabla', miCallback);

No todo lo que brilla es oro 👑

Los callbacks funcionan y es todo lo que NodeJS necesita para funcionar y poder mantener su asincronía y su modelo no bloqueante. Pero a medida que se fueron usando cada vez más y el código del servidor se fue complejizando, empezaron a aparecer problemas...

  • Es casi imposible utilizar try / catchs y loops como forEach y while
  • Es difícil de implementar el manejo de errores

Y lo más importante, se vuelve imposible de leer y entender. Uno de sus problemas más importantes es el llamado callback hell. El mismo se puede representar con el siguiente ejemplo.

El siguiente código consiste en leer 3 archivos e imprimir su contenido

// Leo el archivo 1.txt
fs.readFile('/1.txt', function(error, contenido) {
  console.log(contenido);
  // Leo el archivo 2.txt
  fs.readFile('/2.txt', function(error, contenido) {
    console.log(contenido);
    // Leo el archivo 3.txt
    fs.readFile('/3.txt', function(error, contenido) {
      console.log(contenido);
    });
  });
});

Como podemos ver, terminamos concatenando callbacks. Si tuviéramos un código más complejo con cierta lógica podría volverse inmanejable. Lo peor de todo, es que no manejamos los errores. Veamos cómo quedaría el código si agregamos el manejo de errores.

NoThankYou

Pensaban que iba a hacerlo. No, gracias!. Por suerte existe algo mejor, algo que nació para reemplazar los callbacks para siempre, las Promesas. Pero esto lo veremos en otro capitulo.

Gracias por leer 👋. Recuerden seguirme en Twitter y subscribirse a mi canal de Youtube.