Manejo de excepciones y errores en Dart

Control de errores: Exceptions y Errors en Dart

Para escribir código robusto, necesitas distinguir entre lo que es un fallo en la lógica de tu programa y lo que es un evento inesperado en el entorno. En Dart, esto se separa en dos grandes ramas: los Error y las Exception.

Un Error representa fallos en el programa que no deberían ser capturados, como un error de memoria o un índice fuera de rango; son problemas de programación que indican que el código está mal escrito. Por el contrario, una Exception representa condiciones que un programa bien escrito puede y debe manejar, como la pérdida de conexión a internet o un archivo que no existe.

Dart no utiliza “checked exceptions” como Java; esto significa que no estás obligado a declarar en la firma de una función qué tipo de error puede lanzar. Aunque técnicamente puedes lanzar cualquier Object (como un simple String), la buena práctica dicta que siempre lances objetos que implementen la interfaz Exception.

Cuando algo falla, el flujo de ejecución se detiene y se “lanza” (mediante la palabra clave throw) el objeto del error. Si no hay nadie escuchando, el programa colapsa. Para evitarlo, usamos el bloque try para envolver el código sospechoso, on para filtrar errores por su tipo específico, catch para capturar el objeto del error y su rastro de ejecución (stackTrace), y finally para asegurar que ciertas tareas de limpieza se ejecuten siempre.

Si capturas una excepción pero no puedes resolverla en ese punto, puedes usar rethrow para volver a lanzarla, manteniendo intacto el rastro original de dónde ocurrió el problema, lo cual es vital para la depuración.

class TemperaturaException implements Exception {
  final String mensaje;
  TemperaturaException(this.mensaje);

  @override
  String toString() => 'TemperaturaException: $mensaje';
}

// Simulamos un sensor que devuelve datos de temperatura
String leerSensor() {
  // En un caso real, esto podría venir de un archivo o una API.
  // Si devolviera algo que no es un número, lanzaría un FormatException.
  return "25"; 
}

void procesarClima() {
  final lectura = leerSensor();
  print('Lectura del sensor: $lectura');

  // Intentamos convertir el String a un entero
  final temperatura = int.parse(lectura);

  if (temperatura > 50) {
    // Lanzamos una excepción personalizada si la temperatura es peligrosa
    throw TemperaturaException('El calor es excesivo para el sensor.');
  }

  if (temperatura < -10) {
    // Simulamos un error de lógica/programación si algo fuera de rango
    throw RangeError('Temperatura fuera de los límites de diseño del sensor.');
  }

  print('Temperatura procesada con éxito: $temperatura°C');
}

void main() {
  try {
    procesarClima();
  } on TemperaturaException catch (e) {
    // Capturamos solo nuestra excepción personalizada
    print('Error controlado: $e');
  } on FormatException catch (e, stackTrace) {
    // Capturamos errores de formato (ej. int.parse fallido)
    // El stackTrace nos dice exactamente en qué línea falló el parseo
    print('Error de formato detectado: $e');
    print('Rastro del error:\n$stackTrace');
  } on RangeError catch (e) {
    // Capturamos un Error (de programación) si decidimos manejarlo
    print('Error de rango detectado: $e');
  } catch (e) {
    // Si ocurre cualquier otra cosa que no especificamos arriba
    print('Error inesperado: $e');
  } finally {
    // Este bloque se ejecuta pase lo que pase (éxito o error)
    // Ideal para cerrar conexiones o liberar recursos.
    print('Cerrando conexión con el sensor...');
  }
}

En el ejemplo anterior, el flujo principal ocurre dentro de procesarClima. Cuando ejecutamos int.parse(lectura), Dart intenta convertir el string. Si el string fuera "caliente", el método lanzaría una FormatException. Al usar on FormatException en el main, interceptamos ese error específico de forma limpia.

Fíjate en la importancia del orden de las cláusulas on. Dart evalúa las excepciones de arriba hacia abajo; la primera que coincida con el tipo lanzado es la que se ejecuta. Si hubiéramos puesto un on Exception antes que on TemperaturaException, nunca llegaríamos a capturar la excepción personalizada por separado porque on Exception es más genérica.

Cuando ocurre un error, el segundo parámetro de catch (el stackTrace) es una pieza de información crítica. No es solo un mensaje de texto; es un mapa que muestra la secuencia de llamadas de funciones que llevaron hasta ese punto exacto. Sin esto, en aplicaciones complejas, sería casi imposible saber qué función llamó a cuál antes de que todo colapsara.

El bloque finally garantiza la integridad del sistema. Aunque procesarClima termine con un error de temperatura o con éxito, el mensaje de “Cerrando conexión…” se imprimirá siempre, asegurando que no dejemos recursos abiertos.

El error frecuente

Un error común cuando quieres relanzar una excepción es escribir throw e; en lugar de usar rethrow;.

// ❌ MAL: Pierdes el rastro original del error
try {
  procesarClima();
} catch (e) {
  print('Algo falló, intentando loguear...');
  throw e; // Esto resetea el stackTrace al punto donde estás ahora
}

// ✅ BIEN: Preservas el rastro de dónde ocurrió el error real
try {
  procesarClima();
} catch (e) {
  print('Algo falló, intentando loguear...');
  rethrow; // El stackTrace original se mantiene intacto
}

Si usas throw e, el rastro del error (stack trace) dirá que el error ocurrió en la línea del throw e, ocultando la línea real donde el sensor falló. Usar rethrow es fundamental para que el sistema de logs o el desarrollador pueda localizar el origen real del fallo en la jerarquía de llamadas.

61

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio