26. Validación de datos y manejo de casos no válidos

Una fórmula correcta puede producir un resultado inútil si recibe datos vacíos, infinitos o incompatibles. Validar significa proteger el contrato antes de comenzar el cálculo.

26.1 Por qué validar

Los datos de una interfaz llegan como texto y pueden estar incompletos, mal escritos o fuera del dominio financiero adoptado. JavaScript también posee valores especiales como NaN e Infinity que pueden propagarse silenciosamente.

entrada válida → cálculo confiable → resultado interpretable

Si falla la entrada, el programa debe detener el cálculo y explicar el problema.

Validar no demuestra que una decisión financiera sea conveniente; comprueba que los datos cumplen el contrato matemático y técnico de la función.

26.2 Cuatro etapas separadas

Leertexto de entrada
Convertira números
Validartipo y dominio
Calculary presentar

El orden es importante. No aplicamos la fórmula hasta saber que los valores convertidos son finitos y satisfacen las reglas del modelo.

26.3 Una entrada vacía no es cero

La conversión Number("") devuelve 0. Sin una comprobación previa, un campo vacío podría confundirse con un cero ingresado intencionalmente.

function convertirNumero(texto) {
  if (typeof texto !== "string" || texto.trim() === "") {
    return { ok: false, error: "El valor está vacío" };
  }

  const valor = Number(texto);
  if (!Number.isFinite(valor)) {
    return { ok: false, error: "El valor no es un número finito" };
  }

  return { ok: true, valor };
}

for (const entrada of ["", "abc", "2.5"]) {
  console.log(JSON.stringify({ entrada, resultado: convertirNumero(entrada) }));
}

El objeto devuelto distingue explícitamente éxito y error. Una cadena como "2.5" se convierte; "2,5" no es reconocida por Number y requiere una política de normalización específica.

26.4 Comprobar números finitos

typeof NaN devuelve "number", por lo que verificar solamente el tipo no alcanza. Utilizamos Number.isFinite:

ValorNumber.isFiniteInterpretación
250000trueNúmero finito
NaNfalseResultado numérico indefinido
InfinityfalseValor sin límite finito
"250000"falseEs texto; debe convertirse primero

No conviene reemplazar esta prueba con la función global isFinite, porque realiza conversiones implícitas que pueden ocultar errores.

26.5 Tipo válido y dominio válido

Un dato puede ser numéricamente válido y, aun así, no pertenecer al dominio del problema. Para los ejemplos elementales de crecimiento del curso adoptamos:

Capital

Número finito mayor que cero.

Tasa

Número finito mayor o igual que cero.

Períodos

Entero mayor o igual que cero.

Una tasa negativa puede tener sentido en otros contextos. Rechazarla aquí es una decisión del alcance didáctico, no una limitación universal de JavaScript.

26.6 Validar cantidades de períodos

Number.isInteger permite exigir períodos completos cuando el algoritmo itera una vez por cierre:

function periodosValidos(periodos, maximo = 1200) {
  return Number.isInteger(periodos) &&
    periodos >= 0 &&
    periodos <= maximo;
}

console.log("12:", periodosValidos(12));
console.log("6.5:", periodosValidos(6.5));
console.log("-1:", periodosValidos(-1));
console.log("5000:", periodosValidos(5000));

El límite superior no pertenece a la fórmula; protege recursos de la aplicación. Otro sistema puede elegir un máximo diferente.

26.7 Acumular errores útiles

Detenerse en el primer problema es sencillo, pero en un formulario puede resultar más cómodo informar todos los campos que deben corregirse:

function validarDatos({ capital, tasa, periodos }) {
  const errores = [];

  if (!Number.isFinite(capital) || capital <= 0) {
    errores.push("El capital debe ser un número positivo");
  }
  if (!Number.isFinite(tasa) || tasa < 0) {
    errores.push("La tasa debe ser finita y no negativa");
  }
  if (!Number.isInteger(periodos) || periodos < 0) {
    errores.push("Los períodos deben ser enteros y no negativos");
  }

  return errores;
}

const errores = validarDatos({ capital: 0, tasa: NaN, periodos: 2.5 });
errores.forEach(error => console.log(error));

Los mensajes describen cómo corregir el dato; un simple “valor incorrecto” obliga al usuario a adivinar.

26.8 Un contrato de resultado explícito

En lugar de devolver a veces un número y otras veces un texto, podemos utilizar siempre un objeto con una propiedad ok:

function dividirSeguro(dividendo, divisor) {
  if (!Number.isFinite(dividendo) || !Number.isFinite(divisor)) {
    return { ok: false, errores: ["Los valores deben ser finitos"] };
  }
  if (divisor === 0) {
    return { ok: false, errores: ["El divisor no puede ser cero"] };
  }

  return { ok: true, valor: dividendo / divisor };
}

console.log(JSON.stringify(dividirSeguro(36000, 0.12)));
console.log(JSON.stringify(dividirSeguro(36000, 0)));

Quien utiliza la función consulta primero ok. Así evita confundir un mensaje de error con un resultado financiero.

26.9 Devolver errores o lanzar una excepción

EstrategiaUso habitualResponsabilidad de quien llama
{ ok, valor, errores }Datos esperablemente incorrectos de un formularioMostrar mensajes y no calcular
throw new Error(...)Incumplimiento inesperado del contrato internoCapturar o propagar la excepción

No existe una única estrategia para toda aplicación. Lo importante es que el contrato sea estable y que los errores no se transformen silenciosamente en resultados.

26.10 Validar opciones permitidas

Los textos que seleccionan una estrategia también se validan. No debemos invocar una propiedad inexistente:

const modelos = {
  simple: (capital, tasa, periodos) => capital * (1 + tasa * periodos),
  compuesto: (capital, tasa, periodos) => capital * (1 + tasa) ** periodos
};

function modeloValido(nombre) {
  return Object.hasOwn(modelos, nombre);
}

console.log("simple:", modeloValido("simple"));
console.log("compuesto:", modeloValido("compuesto"));
console.log("desconocido:", modeloValido("desconocido"));

Object.hasOwn comprueba que el nombre corresponda a una propiedad propia del catálogo.

26.11 Una función financiera protegida

Reunimos conversión, validación y cálculo sin mezclar el resultado correcto con los mensajes:

function calcularCompuestoSeguro(capital, tasa, periodos) {
  const errores = [];

  if (!Number.isFinite(capital) || capital <= 0) {
    errores.push("Capital no válido");
  }
  if (!Number.isFinite(tasa) || tasa < 0) {
    errores.push("Tasa no válida");
  }
  if (!Number.isInteger(periodos) || periodos < 0) {
    errores.push("Períodos no válidos");
  }
  if (errores.length > 0) return { ok: false, errores };

  const monto = capital * (1 + tasa) ** periodos;
  if (!Number.isFinite(monto)) {
    return { ok: false, errores: ["El resultado excede el rango numérico"] };
  }

  return { ok: true, valor: { capital, monto, interes: monto - capital } };
}

console.log(JSON.stringify(calcularCompuestoSeguro(200000, 0.05, 4)));
console.log(JSON.stringify(calcularCompuestoSeguro(-10, NaN, 2.5)));

También verificamos la salida: entradas finitas pueden producir un desbordamiento cuando la potencia es extremadamente grande.

26.12 Probar caminos válidos e inválidos

Las pruebas deben recorrer ambos lados de cada condición, no solamente el caso ideal:

function datosValidos(capital, tasa, periodos) {
  return Number.isFinite(capital) && capital > 0 &&
    Number.isFinite(tasa) && tasa >= 0 &&
    Number.isInteger(periodos) && periodos >= 0;
}

const pruebas = [
  { nombre: "caso normal", datos: [100000, 0.05, 6], esperado: true },
  { nombre: "capital vacío convertido", datos: [0, 0.05, 6], esperado: false },
  { nombre: "tasa indefinida", datos: [100000, NaN, 6], esperado: false },
  { nombre: "plazo decimal", datos: [100000, 0.05, 6.5], esperado: false }
];

pruebas.forEach(prueba => {
  const obtenido = datosValidos(...prueba.datos);
  console.log(`${prueba.nombre}: ${obtenido === prueba.esperado ? "correcta" : "falló"}`);
});

Cada error corregido debería convertirse en un nuevo caso de prueba para evitar que reaparezca.

26.13 Laboratorio de validación

Escribí valores válidos o deliberadamente incorrectos. El laboratorio aplica cada regla, marca los campos y solo calcula cuando todas las comprobaciones se cumplen.

Puerta de entrada al cálculo

Leer, convertir, validar y recién entonces aplicar la fórmula.

EstadoDatos válidos
Monto$243.101,25
Interés$43.101,25
Factor1,21550625
Resultado de cada regla de entrada.
CampoTexto recibidoReglaResultado

Mensajes de validación

  • No se encontraron errores.
Operación permitidacalcular("compuesto", 200000, 0.05, 4) → 243101.25

El cálculo se ejecutó porque las cuatro entradas cumplen el contrato.

26.14 Ejercicio propuesto

Implementá validarOperacion para un capital, una tasa porcentual y una cantidad de períodos recibidos como texto.

  1. Rechazá cadenas vacías antes de convertir.
  2. Exigí números finitos.
  3. Exigí capital positivo, tasa entre 0 % y 100 % y períodos enteros entre 0 y 120.
  4. Devolvé todos los mensajes encontrados.
  5. Calculá el monto simple únicamente cuando ok sea verdadero.
Ver solución y código
function validarOperacion(entrada) {
  const errores = [];

  function leer(campo, etiqueta) {
    const texto = entrada[campo];
    if (typeof texto !== "string" || texto.trim() === "") {
      errores.push(`${etiqueta}: valor requerido`);
      return NaN;
    }
    const numero = Number(texto);
    if (!Number.isFinite(numero)) errores.push(`${etiqueta}: número no válido`);
    return numero;
  }

  const capital = leer("capital", "Capital");
  const tasaPorcentaje = leer("tasa", "Tasa");
  const periodos = leer("periodos", "Períodos");

  if (Number.isFinite(capital) && capital <= 0) errores.push("Capital: debe ser positivo");
  if (Number.isFinite(tasaPorcentaje) && (tasaPorcentaje < 0 || tasaPorcentaje > 100)) errores.push("Tasa: fuera de rango");
  if (Number.isFinite(periodos) && (!Number.isInteger(periodos) || periodos < 0 || periodos > 120)) errores.push("Períodos: deben ser enteros entre 0 y 120");

  if (errores.length) return { ok: false, errores };
  const tasa = tasaPorcentaje / 100;
  const monto = capital * (1 + tasa * periodos);
  return { ok: true, valor: { capital, tasa, periodos, monto } };
}

console.log(JSON.stringify(validarOperacion({ capital: "300000", tasa: "2", periodos: "12" })));
console.log(JSON.stringify(validarOperacion({ capital: "", tasa: "abc", periodos: "2.5" })));

26.15 Ideas para recordar

  • Los datos de una interfaz comienzan como texto y deben convertirse explícitamente.
  • Un campo vacío no equivale necesariamente a cero.
  • Number.isFinite excluye NaN e infinitos sin conversiones implícitas.
  • Validar el tipo no reemplaza las reglas del dominio financiero.
  • Los límites de iteración protegen los recursos de la aplicación.
  • Un contrato con ok, valor y errores separa resultados de fallos.
  • Los mensajes deben indicar cómo corregir el dato.
  • Las pruebas incluyen casos válidos, fronteras y entradas incorrectas.

En el próximo tema construiremos tablas de evolución y comparación de escenarios.