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

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
updateConfigque 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.