Saltar al contenido principal

Maneja errores con elegancia

Mejora la robustez de la aplicación manejando errores. Aprende sobre excepciones, errores, `try/catch`, `throw` y `rethrow`.

En este capítulo, aprenderás a hacer que tu aplicación sea más robusta manejando errores de forma elegante. Explorarás excepciones, bloques try/catch y cómo crear excepciones personalizadas para gestionar errores de manera estructurada.

Qué lograrás

Distinguir errores y excepciones
Usar try/catch para manejar fallos
Crear y lanzar excepciones personalizadas

Prerrequisitos

#

Antes de comenzar este capítulo, asegúrate de:

  • Haber completado el Capítulo 5 y tener un entorno de desarrollo de Dart en funcionamiento con el proyecto dartpedia.
  • Entender conceptos básicos de programación como funciones y clases.

Errors versus exceptions

#

Before you start writing code to handle failures, you need to understand the two main types of failures in Dart: errors and exceptions.

  • Exceptions represent conditions that you might expect to happen and that your program can recover from. Examples include invalid user input, a missing file, or a network timeout. Your code can catch and handle exceptions to keep running.
  • Errors represent failure conditions that indicate bugs in the code. Examples include calling a method on a null object, passing an invalid index to a list, or failing to initialize a late variable before use. You don't catch errors. Instead, you let them crash the application, which helps you find and fix the underlying bug.

In Dart, any non-null object can be thrown as an exception. However, by convention, exceptions implement Exception, while errors extend or implement Error. When you design an API, throw subtypes of Exception for conditions that callers can catch and handle.

Tareas

#

En este capítulo, mejorarás la robustez de tu paquete command_runner implementando el manejo de errores. Crearás una clase de excepción personalizada y agregarás manejo de errores al CommandRunner para gestionar de forma elegante los errores que puedan ocurrir durante la ejecución del comando.

Tarea 1: Crear una ArgumentException personalizada

#

Primero, define una clase de excepción personalizada llamada ArgumentException para representar errores relacionados con los argumentos de línea de comandos.

  1. Crea el archivo command_runner/lib/src/exceptions.dart. Este archivo contendrá la definición de tu clase ArgumentException.

  2. Define una clase llamada ArgumentException que extienda (extends) de FormatException.

    command_runner/lib/src/exceptions.dart
    dart
    class ArgumentException extends FormatException {
      /// The command that was parsed before discovering the error.
      ///
      /// This will be empty if the error was on the root parser.
      final String? command;
    
      /// The name of the argument that was being parsed when the error was
      /// discovered.
      final String? argumentName;
    
      ArgumentException(
        super.message, [
        this.command,
        this.argumentName,
        super.source,
        super.offset,
      ]);
    
      @override
      String toString() {
        return 'ArgumentException: $message';
      }
    }
    

    Esta clase extiende de FormatException, que es una clase de excepción integrada en Dart. Incluye propiedades adicionales para almacenar el comando y el nombre del argumento asociados con el error. Esto proporciona más contexto al manejar la excepción.

    • command: El comando que se estaba procesando cuando ocurrió la excepción.
    • argumentName: El nombre del argumento que causó la excepción.

Tarea 2: Implementar el manejo de errores en CommandRunner

#

A continuación, actualiza la clase CommandRunner para manejar posibles errores con elegancia. Esto implica añadir un callback de manejo de errores, usar try/catch para gestionar excepciones y lanzar tu nueva ArgumentException cuando el usuario proporcione una entrada incorrecta.

  1. Agrega los imports necesarios.

    En command_runner/lib/src/command_runner_base.dart, añade imports para dart:async (para usar FutureOr) y tu nuevo archivo exceptions.dart.

    dart
    import 'dart:async'; // Add this line
    import 'dart:collection';
    import 'dart:io';
    
    import 'arguments.dart';
    import 'exceptions.dart'; // Add this line
    
  2. Añade un callback onError al CommandRunner.

    Modifica el CommandRunner para aceptar una función onError opcional en su constructor. Esto permitirá al usuario de tu paquete definir su propia lógica de manejo de errores.

    dart
    class CommandRunner {
      // Add a constructor that accepts the optional callback.
      CommandRunner({this.onError});
    
      final Map<String, Command> _commands = <String, Command>{};
    
      UnmodifiableSetView<Command> get commands =>
          UnmodifiableSetView<Command>(<Command>{..._commands.values});
    
      // Define the onError property.
      FutureOr<void> Function(Object)? onError;
    
      // The rest of the class implementation...
    }
    

    Este cambio introduce una propiedad onError que acepta nulos. El tipo FutureOr<void> Function(Object)? significa que es una función que toma un Object y devuelve un Future o nada (void), y podría ser null.

  3. Actualiza el método run para usar try/catch.

    Envuelve la lógica dentro del método run en un bloque try/catch. Si ocurre una excepción, este bloque la "atrapará" y la pasará al callback onError o la volverá a lanzar (rethrow) si no se proporcionó ningún callback. rethrow conserva el error y la traza de pila (stack trace) originales.

    dart
    Future<void> run(List<String> input) async {
      // [Step 6 update] try/catch added
      try {
        final ArgResults results = parse(input);
        if (results.command != null) {
          Object? output = await results.command!.run(results);
          print(output.toString());
        }
      } on Exception catch (exception) {
        if (onError != null) {
          onError!(exception);
        } else {
          rethrow;
        }
      }
    }
    
  4. Añade validación al método parse.

    Finalmente, reemplaza el método parse existente en command_runner_base.dart con la siguiente versión actualizada. Esta nueva versión es mucho más robusta. Está llena de comprobaciones que lanzarán tu ArgumentException personalizada cada vez que detecte una entrada de usuario no válida.

    dart
    // [Step 6 update] This method is replaced entirely.
    ArgResults parse(List<String> input) {
      ArgResults results = ArgResults();
      if (input.isEmpty) return results;
    
      // Throw an exception if the command is not recognized.
      if (_commands.containsKey(input.first)) {
        results.command = _commands[input.first];
        input = input.sublist(1);
      } else {
        throw ArgumentException(
          'The first word of input must be a command.',
          null,
          input.first,
        );
      }
    
      // Throw an exception if multiple commands are provided.
      if (results.command != null &&
          input.isNotEmpty &&
          _commands.containsKey(input.first)) {
        throw ArgumentException(
          'Input can only contain one command. Got ${input.first} and ${results.command!.name}',
          null,
          input.first,
        );
      }
    
      // Section: Handle options, including flags.
      Map<Option, Object?> inputOptions = {};
      int i = 0;
      while (i < input.length) {
        if (input[i].startsWith('-')) {
          var base = _removeDash(input[i]);
          // Throw an exception if an option is not recognized for the given command.
          var option = results.command!.options.firstWhere(
            (option) => option.name == base || option.abbr == base,
            orElse: () {
              throw ArgumentException(
                'Unknown option ${input[i]}',
                results.command!.name,
                input[i],
              );
            },
          );
    
          if (option.type == OptionType.flag) {
            inputOptions[option] = true;
            i++;
            continue;
          }
    
          if (option.type == OptionType.option) {
            // Throw an exception if an option requires an argument but none is given.
            if (i + 1 >= input.length) {
              throw ArgumentException(
                'Option ${option.name} requires an argument',
                results.command!.name,
                option.name,
              );
            }
            if (input[i + 1].startsWith('-')) {
              throw ArgumentException(
                'Option ${option.name} requires an argument, but got another option ${input[i + 1]}',
                results.command!.name,
                option.name,
              );
            }
            var arg = input[i + 1];
            inputOptions[option] = arg;
            i++;
          }
        } else {
          // Throw an exception if more than one positional argument is provided.
          if (results.commandArg != null && results.commandArg!.isNotEmpty) {
            throw ArgumentException(
              'Commands can only have up to one argument.',
              results.command!.name,
              input[i],
            );
          }
          results.commandArg = input[i];
        }
        i++;
      }
      results.options = inputOptions;
    
      return results;
    }
    
    String _removeDash(String input) {
      if (input.startsWith('--')) {
        return input.substring(2);
      }
      if (input.startsWith('-')) {
        return input.substring(1);
      }
      return input;
    }
    

    Este método parse actualizado ahora se defiende activamente contra las entradas incorrectas. Específicamente, las nuevas sentencias throw manejan varios casos de error comunes:

    • Comandos desconocidos: El primer bloque if/else asegura que el primer argumento sea un comando válido.
    • Múltiples comandos: Verifica que el usuario no haya intentado ejecutar más de un comando a la vez.
    • Opciones desconocidas: El parámetro orElse dentro de firstWhere ahora lanza una excepción si un usuario proporciona una bandera u opción (como --foo) que no ha sido definida para ese comando.
    • Valores de opción ausentes: Asegura que una opción (como --output) vaya seguida de un valor y no de otra opción o del final de la entrada.
    • Demasiados argumentos: Impone la regla de que los comandos solo pueden tener un argumento posicional.

Tarea 3: Actualizar cli.dart para usar el nuevo manejo de errores

#

Modifica cli/bin/cli.dart para usar el nuevo manejo de errores en CommandRunner.

  1. Abre el archivo cli/bin/cli.dart.

  2. Actualiza la función main para pasar una función onError al CommandRunner:

    cli/bin/cli.dart
    dart
    import 'package:command_runner/command_runner.dart';
    
    const version = '0.0.1';
    
    void main(List<String> arguments) {
      // [Step 6 update] Add onError method
      var commandRunner = CommandRunner(
        onError: (Object error) {
          if (error is Error) {
            throw error;
          }
          if (error is Exception) {
            print(error);
          }
        },
      )..addCommand(HelpCommand());
      commandRunner.run(arguments);
    }
    

    This code passes in an onError callback function to the CommandRunner constructor. If an error occurs during the execution of a command, the onError callback function is called with the error object. The callback checks whether the error is an Error or an Exception. Because an Error indicates a bug in the code, the callback rethrows it. This lets the application crash so you can fix the bug. For an Exception, which represents a recoverable failure, the callback handles it gracefully by printing it to the console instead of rethrowing it.

Tarea 4: Actualizar las exportaciones de la librería command_runner

#

Haz que ArgumentException esté disponible para la librería command_runner.

  1. Abre command_runner/lib/command_runner.dart y añade las siguientes exportaciones:

    command_runner/lib/command_runner.dart
    dart
    /// Support for doing something awesome.
    ///
    /// More dartdocs go here.
    library;
    
    export 'src/arguments.dart';
    export 'src/command_runner_base.dart';
    export 'src/help_command.dart';
    export 'src/exceptions.dart'; // Add this line
    
    // TODO: Export any libraries intended for clients of this package.
    

    Esto asegura que la ArgumentException esté disponible para los consumidores del paquete command_runner.

Tarea 5: Probar el nuevo manejo de errores

#

Prueba el nuevo manejo de errores ejecutando la aplicación con argumentos inválidos.

  1. Abre tu terminal y navega al directorio cli.

  2. Ejecuta el comando dart run bin/cli.dart invalid_command.

    Deberías ver la siguiente salida:

    bash
    ArgumentException: The first word of input must be a command.
    

    Esto confirma que la ArgumentException se está lanzando y capturando correctamente.

Revisión

#

Qué lograste

Aquí tienes un resumen de lo que construiste y aprendiste en esta lección.
Distinguiste errores de excepciones

You learned that subtypes of Error indicate programming bugs and aren't intended to be caught, while subtypes of Exception represent recoverable failures that your code can handle gracefully.

Usaste bloques try-catch para manejar fallos

Envolviste el código de riesgo en bloques try-catch para interceptar excepciones. Usaste on ExceptionType catch (e) para manejar tipos específicos y rethrow para volver a propagar excepciones conservando la traza de pila (stack trace).

Creaste y lanzaste excepciones personalizadas

Creaste una clase ArgumentException que extiende de FormatException para proporcionar información de error enriquecida con contexto. Luego usaste throw para señalar fallos de validación con mensajes significativos para una mejor depuración y comentarios para el usuario.

Cuestionario

#

Pon a prueba tu comprensión

1 / 3
You're writing a function that parses user input. If the input is invalid, what's the best way to signal this to the calling code?
  1. Devolver null y dejar que el llamador lo verifique.

    No exactamente.

    Devolver null obliga a los llamadores a acordarse de verificarlo. Si se olvidan, pueden ocurrir errores de forma silenciosa. Hay una forma más explícita de señalar problemas.

  2. Lanzar una excepción que describa lo que salió mal.

    ¡Correcto!

    Lanzar una excepción detiene inmediatamente la ruta del código inválido, proporciona un mensaje de error claro y obliga al llamador a manejar el error explícitamente.

  3. Imprimir un mensaje de error y continuar la ejecución.

    No exactamente.

    Imprimir no evita que se utilicen datos inválidos. La función aún necesitaría devolver algo, lo que potencialmente causaría errores más adelante.

  4. Establecer una bandera de error global que otro código pueda verificar.

    No exactamente.

    El estado global es propenso a errores y fácil de olvidar. Este enfoque no obliga a los llamadores a manejar el error.

What's the difference between throw and rethrow in a catch block?
  1. throw es para errores; rethrow es para excepciones.

    No exactamente.

    Ambos funcionan con cualquier objeto lanzable. La diferencia no radica en el tipo de objeto que se lanza.

  2. throw siempre crea una nueva traza de pila; rethrow conserva la traza de pila original.

    ¡Correcto!

    Usa rethrow cuando quieras registrar o manejar parcialmente una excepción pero aún así dejar que se propague con su traza de pila original intacta para la depuración.

  3. throw funciona dentro de los bloques catch, mientras que rethrow solo funciona fuera de ellos.

    No exactamente.

    Es casi lo contrario. rethrow solo funciona dentro de bloques catch. throw funciona en cualquier lugar.

  4. throw crea una nueva excepción; rethrow solo imprime la actual.

    No exactamente.

    Ninguno de los dos imprime nada. rethrow propaga la excepción, no solo la muestra.

Consider this code: try { riskyOperation(); } on FormatException catch (e) { print(e); }. What happens if riskyOperation() throws an HttpException?
  1. La HttpException se propaga hacia arriba, sin ser capturada por este try/catch.

    ¡Correcto!

    La cláusula on filtra por tipo de excepción. Dado que HttpException no es una FormatException, evade este bloque catch por completo.

  2. La excepción se ignora silenciosamente y la ejecución continúa.

    No exactamente.

    Las excepciones nunca se ignoran silenciosamente en Dart. Si un bloque catch no coincide, la excepción no desaparece simplemente.

  3. El programa falla (se bloquea) porque no puedes usar on con catch.

    No exactamente.

    on Type catch (e) es una sintaxis válida que filtra qué excepciones capturar. El problema es la discrepancia de tipos, no la sintaxis.

  4. La HttpException es capturada e impresa.

    No exactamente.

    La cláusula on FormatException solo captura FormatException y sus subtipos. HttpException es un tipo diferente.

Siguiente lección

#

En la siguiente lección, aprenderás sobre características avanzadas orientadas a objetos en Dart, incluyendo enums mejorados y extensiones. Mejorarás el formato de salida y añadirás color a tu aplicación de CLI.