Error vs Exception: la distinción que salva tu lógica

En Dart, confundir un Error con una Exception no es solo un error de semántica; es un error de diseño que compromete la integridad de tu aplicación. Un Error representa un fallo en la lógica del programador, algo que no debería ocurrir si el código está escrito correctamente (como un RangeError al acceder a un índice inexistente o un LateInitializationError al usar una variable late sin inicializar). Por el contrario, una Exception representa una condición que puede ocurrir durante la ejecución normal, incluso con el código perfecto (como un FormatException al procesar un JSON malformado o un TimeoutException en una petición de red).

La razón de esta distinción reside en la semántica del flujo de control: los Error indican que el estado interno de la aplicación es inconsistente y que la máquina virtual no puede garantizar la seguridad de la ejecución, por lo que el programa debe detenerse. Las Exception, en cambio, son eventos predecibles que el desarrollador debe manejar para que la aplicación siga funcionando.

Debes usar Error para fallos que detectes mediante validaciones de contrato interno o aserciones. Debes usar Exception para fallos que dependan de factores externos o datos de entrada que no controlas. Si intentas capturar un Error con un bloque try-catch, podrías estar ocultando un bug que debería corregirse en la fase de desarrollo, dejando la aplicación en un estado “zombi” donde el flujo continúa pero la lógica está rota. Si ignoras una Exception, tu programa simplemente podría fallar silenciosamente o quedar en un estado de espera indefinido.

// Para crear excepciones personalizadas, implementamos la interfaz Exception
class UsuarioInvalidoException implements Exception {
  final String mensaje;
  UsuarioInvalidoException(this.mensaje);

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

/// Procesa los datos de un usuario provenientes de una fuente externa (ej. un JSON).
void procesarUsuario(Map<String, dynamic> datos) {
  // 1. Error de argumento: El contrato exige un 'id'. 
  // ArgumentError.notNull [disponible desde Dart 2.12] es ideal para contratos.
  final id = datos['id'] ?? throw ArgumentError.notNull('id');

  // 2. Exception de formato: El dato externo puede venir mal estructurado.
  final edadRaw = datos['age'];
  final int edad = int.tryParse(edadRaw.toString()) ?? 
      throw FormatException('La edad "$ageRaw" no es un número válido.');

  // 3. Exception de negocio: La edad es lógicamente imposible para nuestro sistema.
  if (edad < 0 || edad > 150) {
    throw UsuarioInvalidoException('Edad $edad fuera de rango permitido.');
  }

  print('✅ Usuario $id procesado: $edad años.');
}

void main() {
  final entradas = [
    {'id': 101, 'age': '25'},      // Caso correcto
    {'id': 102, 'age': 'veinticinco'}, // FormatException (Exception)
    {'id': 103, 'age': -5},        // UsuarioInvalidoException (Exception)
    {'age': 30},                   // ArgumentError (Error)
  ];

  for (var entrada in entradas) {
    try {
      procesarUsuario(entrada);
    } on FormatException catch (e) {
      print('⚠️ Error de formato detectado: ${e.message}');
    } on UsuarioInvalidoException catch (e) {
      print('🚫 Error de negocio: ${e.toString()}');
    } catch (e) {
      // Capturamos cualquier cosa que no sea una Exception controlada.
      // Si el error es un 'Error' (como ArgumentError), lo dejamos volar.
      if (e is Error) {
        print('❌ CRITICAL ERROR (Bug detectado): $e');
        rethrow; 
      }
      print('❓ Error desconocido: $e');
    }
  }

  // Demostración de un Error que no se puede (ni se debe) manejar para continuar.
  final lista = [1, 2, 3];
  print(lista[10]); // Lanzará un RangeError (Error) y detendrá la ejecución.
}

En el ejemplo anterior, fíjate cómo manejamos procesarUsuario. Cuando datos['id'] es nulo, lanzamos un ArgumentError.notNull('id'). Esto es un Error porque el programador ha violado el contrato de la función; no es algo que la aplicación deba “gestionar” con un mensaje de error al usuario, sino algo que debe arreglarse en el código.

Por otro lado, cuando int.tryParse falla, lanzamos una FormatException. Esto es una Exception porque es un error externo (el usuario escribió “veinticinco” en vez de “25”) y es algo que el flujo normal de la aplicación debe ser capaz de capturar y manejar. Lo mismo ocurre con UsuarioInvalidoException: es una excepción de lógica de negocio que implementamos extendiendo Exception para que el programador pueda diferenciarla de otros fallos técnicos.

Finalmente, cuando intentamos acceder a lista[10], la VM lanza un RangeError. Este es un Error de tipo RangeError que indica que nuestra lógica de índices es errónea. No lo capturamos en un bloque on Error porque, si lo hiciéramos, el programa seguiría ejecutándose con un índice inválido, lo que llevaría a un comportamiento impredecible.

El error frecuente

El error más peligroso es intentar “silenciar” errores con bloques catch genéricos para evitar que la aplicación se cierre.

try {
  final valor = lista[10]; // RangeError (Error)
} catch (e) {
  // ERROR: Estás ocultando un bug de lógica.
  print('Algo salió mal, pero la app sigue...');
}

Si capturas un Error como si fuera una Exception, estás ocultando un bug. Si el RangeError ocurre porque un algoritmo de paginación está mal calculado, la aplicación continuará su ejecución, pero en un estado inconsistente, lo que provocará fallos mucho más difíciles de depurar en otras partes del sistema más adelante.

Si aprendes a distinguir estos dos conceptos, tus trazas de error en producción serán mucho más útiles: los Error te indicarán dónde tienes bugs que arreglar, y las Exception te indicarán qué problemas están sufriendo tus usuarios en el mundo real.

62

Dejar un comentario

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

Scroll al inicio