Closures en JavaScript: cómo crear utilidades configurables y reutilizables

Close-up view of colorful programming code on a screen, ideal for tech and development themes.
Foto de Leonid Altman en Pexels

Los closures son una de esas características de JavaScript que, una vez que las dominas, cambian la forma en que estructuramos nuestras utilidades. En este artículo vamos a ver cómo aprovecharlos para crear funciones configurables que puedes reutilizar en cualquier proyecto, sin recurrir a patrones más pesados o a librerías externas.

¿Por qué una función configurable?

Imagina que necesitas un pequeño logger que incluya información de contexto (módulo, nivel, timestamp) y que, además, puedas activar o desactivar en tiempo de ejecución. En lugar de escribir varias versiones de la misma función o pasar parámetros en cada llamada, podemos usar un closure para "encerrar" la configuración y devolver una función lista para usar.

Primer paso: la fábrica de funciones

Crearemos una createLogger que recibe un objeto de opciones y devuelve la función log. La magia está en que log sigue teniendo acceso a options aunque createLogger haya terminado su ejecución.

function createLogger({ prefix = '', level = 'info', enabled = true } = {}) {
  const levels = { debug: 0, info: 1, warn: 2, error: 3 };
  const currentLevel = levels[level];

  return function log(message, msgLevel = 'info') {
    if (!enabled) return;
    if (levels[msgLevel] < currentLevel) return;
    const timestamp = new Date().toISOString();
    console.log(`${timestamp} ${prefix}[${msgLevel}] ${message}`);
  };
}

// Uso
const logger = createLogger({ prefix: '🛠️ ', level: 'debug' });
logger('Inicializando módulo');
logger('Algo raro ocurrió', 'warn');

En este ejemplo, log mantiene una referencia a options y a la tabla levels. Cada vez que la llamas, la configuración está disponible sin necesidad de pasarla de nuevo.

Aplicación práctica: wrapper de fetch con retry automático

Otro caso típico es crear una capa alrededor de fetch que añada lógica de reintentos, cabeceras comunes o gestión de tokens. Con un closure podemos encapsular toda esa lógica y devolver una función sencilla de usar.

function createFetchWrapper({ baseUrl = '', headers = {}, retries = 2 } = {}) {
  return async function request(endpoint, options = {}) {
    const config = {
      ...options,
      headers: { ...headers, ...(options.headers || {}) },
    };
    let attempt = 0;
    while (attempt <= retries) {
      try {
        const response = await fetch(`${baseUrl}${endpoint}`, config);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return await response.json();
      } catch (err) {
        if (attempt === retries) throw err;
        attempt++;
        console.warn(`Reintento ${attempt}/${retries} para ${endpoint}`);
      }
    }
  };
}

// Uso
const api = createFetchWrapper({
  baseUrl: 'https://api.ejemplo.com/',
  headers: { Authorization: 'Bearer xyz' },
  retries: 3,
});

api('users/123')
  .then(data => console.log(data))
  .catch(err => console.error('Falló la petición:', err));

La función request recuerda baseUrl, headers y retries sin que tengas que pasar esos valores en cada llamada. Además, el código queda mucho más legible que un montón de parámetros sueltos.

Ventajas de este enfoque

  • Separación de responsabilidades: la configuración está aislada, la lógica de negocio se mantiene limpia.
  • Reutilización: puedes crear distintas instancias con configuraciones diferentes (por ejemplo, varios loggers para módulos distintos).
  • Testabilidad: al inyectar la configuración, es sencillo mockearla en pruebas unitarias.
  • Performance: el closure se crea una sola vez; las llamadas posteriores solo ejecutan la lógica interna.

Consejos para evitar errores comunes

  • No sobrecargues el closure con demasiada información; si la configuración crece, considera pasar un objeto que contenga sólo lo esencial.
  • Si necesitas actualizar la configuración en tiempo de ejecución, devuelve también un método updateConfig que modifique la referencia interna.
  • Recuerda que los closures mantienen referencias a variables externas, lo que puede impedir que el garbage collector libere memoria si no los limpias correctamente.

Resumen

Los closures no son sólo una curiosidad del lenguaje; son una herramienta práctica para crear funciones configurables, encapsular lógica y mantener tu código modular. Ya sea un logger, un wrapper de fetch o cualquier otra utilidad, aprovechar los closures te ahorrará código repetitivo y te dará mayor control sobre el comportamiento de tus funciones.