Pular para o conteúdo principal

Trate erros com elegância

Torne o app mais robusto tratando erros. Aprenda sobre exceções, erros, `try/catch`, `throw` e `rethrow`.

Neste capítulo, você vai tornar sua aplicação mais robusta tratando erros com elegância. Explore exceções, blocos try/catch e como criar exceções personalizadas para gerenciar erros de forma estruturada.

O que você vai realizar

Diferenciar erros de exceções
Usar try/catch para tratar falhas
Criar e lançar exceções personalizadas

Pré-requisitos

#

Antes de começar este capítulo, certifique-se de:

  • Ter concluído o Capítulo 6 e ter um ambiente de desenvolvimento Dart funcionando com o projeto dartpedia.
  • Entender conceitos básicos de programação, como funções e classes.

Erros versus exceções

#

O Dart distingue dois tipos principais de falhas: erros e exceções.

Conceito Exception Error
O que indica Uma falha esperada em tempo de execução, da qual o código pode se recuperar. Um bug de programação ou uma falha no código.
Exemplos comuns FormatException, HttpException, SocketException. RangeError, TypeError, StateError.
Causas típicas Entrada inválida do usuário, falha de rede, arquivo ausente. Índice fora por um, chamada de método em null.
Como tratar Capturar e tratar com elegância (por exemplo, com try/catch). Não capturar; corrija o bug no seu código.

Exceções

#

Exceções representam condições que você pode prever e das quais pode se recuperar. Por exemplo, quando o usuário digita um argumento de linha de comando não reconhecido, seu aplicativo não deve travar com um stack trace bruto. Em vez disso, seu código captura a exceção, exibe uma mensagem de erro amigável e mostra ao usuário as instruções de uso corretas.

No Dart, exceções personalizadas normalmente implementam a classe Exception ou estendem um tipo de exceção existente, como FormatException.

Erros

#

Erros representam bugs na sua lógica que devem ser corrigidos durante o desenvolvimento, e não tratados em tempo de execução. Por exemplo, acessar um elemento além dos limites de uma lista lança um RangeError. Tentar capturar e suprimir um RangeError esconde o bug e pode deixar seu aplicativo em um estado imprevisível. Em vez disso, deixe o erro se propagar para que você possa inspecionar o stack trace e corrigir o erro original.

No Dart, classes que representam bugs estendem ou implementam Error.

Tarefas

#

As tarefas a seguir aplicam esses princípios ao pacote command_runner. Você vai definir uma classe de exceção personalizada para argumentos inválidos do usuário, usar blocos try/catch para interceptar falhas e configurar o CommandRunner para tratar exceções com elegância enquanto deixa erros inesperados se propagarem.

Tarefa 1: Crie uma ArgumentException personalizada

#

Primeiro, defina uma classe de exceção personalizada chamada ArgumentException para representar erros relacionados a argumentos de linha de comando.

  1. Crie o arquivo command_runner/lib/src/exceptions.dart. Esse arquivo contém a definição da sua classe ArgumentException.

  2. Defina uma classe chamada ArgumentException que extends 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';
      }
    }
    

    Essa classe estende FormatException, que é uma classe integrada do Dart que implementa Exception. Argumentos de linha de comando inválidos são uma condição esperada que quem chama pode prever e tratar com elegância, por isso ArgumentException é projetada como uma exceção, e não como um Error. Ela inclui propriedades adicionais para armazenar o comando e o nome do argumento associados ao erro. Isso fornece mais contexto ao tratar a exceção.

    • command: O comando que estava sendo processado quando a exceção ocorreu.
    • argumentName: O nome do argumento que causou a exceção.

Tarefa 2: Implemente o tratamento de erros no CommandRunner

#

Em seguida, atualize a classe CommandRunner para tratar possíveis erros com elegância. Isso envolve adicionar um callback de tratamento de erros, usar try/catch para gerenciar exceções e lançar sua nova ArgumentException quando o usuário fornecer uma entrada inválida.

  1. Adicione os imports necessários.

    Em command_runner/lib/src/command_runner_base.dart, adicione imports para dart:async (para usar FutureOr) e para o seu novo arquivo exceptions.dart.

    command_runner/lib/src/command_runner_base.dart
    dart
    import 'dart:async'; // Add this line
    import 'dart:collection';
    import 'dart:io';
    ​
    import 'arguments.dart';
    import 'exceptions.dart'; // Add this line
    
  2. Adicione um callback onError ao CommandRunner.

    Modifique o CommandRunner para aceitar uma função onError opcional em seu construtor. Isso permite que quem usa seu pacote defina a própria lógica de tratamento de erros.

    command_runner/lib/src/command_runner_base.dart
    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...
    }
    

    Essa mudança introduz a propriedade onError, que pode ser nula. O tipo FutureOr<void> Function(Object)? significa que é uma função que recebe um Object e retorna um Future ou nada, e pode ser nula.

  3. Atualize o método run para usar try/catch.

    Envolva a lógica dentro do método run em um bloco try/catch. Se ocorrer uma exceção, esse bloco a captura e a envia para o callback onError ou a relança caso nenhum callback seja fornecido. O rethrow preserva o erro original e o stack trace.

    command_runner/lib/src/command_runner_base.dart
    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;
        }
      }
    }
    

    Observe o uso de on Exception catch (exception). No Dart, um catch (e) sem tipo intercepta todos os objetos lançados, incluindo instâncias de Error. Especificar on Exception garante que seu código capture apenas exceções recuperáveis, deixando os bugs se propagarem sem serem capturados para que você possa corrigi-los.

  4. Adicione validação ao método parse.

    Por fim, substitua o método parse existente em command_runner_base.dart pela versão atualizada a seguir. Ela inclui verificações que lançam sua ArgumentException personalizada sempre que detectam uma entrada inválida do usuário.

    command_runner/lib/src/command_runner_base.dart
    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 atualizado agora se protege ativamente contra entradas inválidas. Especificamente, os novos statements throw tratam vários casos comuns de erro:

    • Comandos desconhecidos: O primeiro bloco if/else garante que o primeiro argumento seja um comando válido.
    • Vários comandos: Ele verifica se o usuário não tentou executar mais de um comando por vez.
    • Opções desconhecidas: O parâmetro orElse dentro de firstWhere agora lança uma exceção se o usuário fornecer uma flag ou opção (como --foo) que não foi definida para aquele comando.
    • Valores de opção ausentes: Ele garante que uma opção (como --output) seja seguida de um valor, e não de outra opção ou do fim da entrada.
    • Argumentos demais: Ele aplica a regra de que comandos podem ter apenas um argumento posicional.

Tarefa 3: Atualize o cli.dart para usar o novo tratamento de erros

#

Modifique cli/bin/cli.dart para usar o novo tratamento de erros no CommandRunner.

  1. Abra o arquivo cli/bin/cli.dart.

  2. Atualize a função main para passar uma função onError ao 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);
    }
    

    Este código passa um callback onError para o CommandRunner. O callback relança qualquer Error, fazendo o aplicativo travar, enquanto imprime qualquer Exception no console para que o usuário veja a mensagem de erro.

Tarefa 4: Atualize as exportações da biblioteca command_runner

#

Torne ArgumentException disponível para a biblioteca command_runner.

  1. Abra command_runner/lib/command_runner.dart e adicione as seguintes exportações:

    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.
    

    Em pacotes Dart, os arquivos dentro de lib/src/ são detalhes de implementação privados. Exportar src/exceptions.dart a partir de lib/command_runner.dart expõe ArgumentException como parte da API pública do pacote, para que quem o usa (como o cli.dart) possa importá-la e usá-la.

Tarefa 5: Teste o novo tratamento de erros

#

Teste o novo tratamento de erros executando o aplicativo com argumentos inválidos.

  1. Abra o terminal e navegue até o diretório cli.

  2. Execute o comando dart run bin/cli.dart invalid_command.

    Você deve ver a seguinte saída:

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

    Isso confirma que a ArgumentException está sendo lançada e capturada corretamente.

Revisão

#

O que você realizou

Veja um resumo do que você construiu e aprendeu nesta lição.
Diferenciou erros de exceções

Você aprendeu que subtipos de Error indicam bugs de programação e não devem ser capturados, enquanto subtipos de Exception representam falhas recuperáveis que o seu código pode tratar com elegância.

Usou blocos try-catch para tratar falhas

Você envolveu código arriscado em blocos try-catch para interceptar exceções. Você usou on ExceptionType catch (e) para tratar tipos específicos e rethrow para relançar exceções preservando o stack trace.

Criou e lançou exceções personalizadas

Você criou uma classe ArgumentException que estende FormatException para fornecer informações de erro ricas em contexto. Depois, usou throw para sinalizar falhas de validação com mensagens significativas, para facilitar a depuração e o retorno ao usuário.

Quiz

#

Confira seu entendimento

1 / 4
Você está escrevendo uma função que interpreta a entrada do usuário. Se a entrada for inválida, qual é a melhor forma de sinalizar isso para o código que chamou a função?
  1. Lançar uma exceção descrevendo o que deu errado.

    Isso mesmo!

    Lançar uma exceção interrompe de imediato o caminho inválido, fornece uma mensagem de erro clara e obriga quem chamou a função a tratar o erro explicitamente.

  2. Retornar null e deixar quem chamou verificar.

    Não exatamente.

    Retornar null obriga quem chama a lembrar de verificar. Se esquecer, os bugs podem acontecer silenciosamente. Existe uma forma mais explícita de sinalizar problemas.

  3. Imprimir uma mensagem de erro e continuar a execução.

    Não exatamente.

    Imprimir não impede que dados inválidos sejam usados. A função ainda precisaria retornar algo, o que pode causar bugs mais adiante.

  4. Definir uma flag global de erro que outro código possa consultar.

    Não exatamente.

    Estado global é propenso a erros e fácil de esquecer. Essa abordagem não obriga quem chama a tratar o erro.

Qual é a diferença entre throw e rethrow em um bloco catch?
  1. throw sempre cria um novo stack trace; rethrow preserva o stack trace original.

    Isso mesmo!

    Use rethrow quando quiser registrar ou tratar parcialmente uma exceção, mas ainda deixá-la se propagar com o stack trace original intacto para depuração.

  2. throw cria uma nova exceção; rethrow apenas imprime a atual.

    Não exatamente.

    Nenhum dos dois imprime nada. rethrow propaga a exceção, não apenas a exibe.

  3. throw funciona dentro de blocos catch, enquanto rethrow só funciona fora deles.

    Não exatamente.

    É quase o contrário. rethrow só funciona dentro de blocos catch. throw funciona em qualquer lugar.

  4. throw serve para erros; rethrow serve para exceções.

    Não exatamente.

    Os dois funcionam com qualquer objeto lançável. A diferença não está no tipo do objeto lançado.

Considere este código: try { riskyOperation(); } on FormatException catch (e) { print(e); }. O que acontece se riskyOperation() lançar um HttpException?
  1. O HttpException se propaga, sem ser capturado por este try/catch.

    Isso mesmo!

    A cláusula on filtra pelo tipo da exceção. Como HttpException não é um FormatException, ela ignora este bloco catch por completo.

  2. O HttpException é capturado e impresso.

    Não exatamente.

    A cláusula on FormatException captura apenas FormatException e seus subtipos. HttpException é outro tipo.

  3. O programa falha porque não é possível usar on com catch.

    Não exatamente.

    on Type catch (e) é uma sintaxe válida que filtra quais exceções capturar. O problema é a incompatibilidade de tipo, não a sintaxe.

  4. A exceção é ignorada silenciosamente e a execução continua.

    Não exatamente.

    No Dart, exceções nunca são ignoradas silenciosamente. Se nenhum bloco catch corresponder, a exceção não simplesmente desaparece.

No Dart, qual é a diferença principal entre um Error e uma Exception?
  1. Um Error indica um bug de programação que não deveria ser capturado, enquanto uma Exception representa uma condição esperada da qual o código pode se recuperar.

    Isso mesmo!

    Subtipos de Error (como RangeError ou TypeError) indicam bugs no código que você deve corrigir. Subtipos de Exception (como FormatException ou HttpException) representam situações que o seu código pode prever e tratar com elegância.

  2. Error é lançado pelo runtime do Dart, enquanto Exception só pode ser lançada pelo código do usuário.

    Não exatamente.

    Tanto o runtime do Dart quanto o código do usuário podem lançar exceções e erros.

  3. Exception exige um bloco try/catch, mas Error é ignorado se não for capturado.

    Não exatamente.

    Erros e exceções não capturados encerram a execução. Nenhum dos dois é ignorado silenciosamente.

  4. Error e Exception são idênticos no Dart e podem ser usados um no lugar do outro.

    Não exatamente.

    O Dart distingue os dois por design: erros indicam bugs, enquanto exceções representam falhas recuperáveis.

Próxima lição

#

Na próxima lição, você vai aprender sobre recursos avançados de orientação a objetos no Dart, incluindo enums aprimorados e extensões. Você também vai melhorar a formatação da saída e adicionar cor ao seu aplicativo CLI.

Encontrou essa página sem tradução ou que precisa de correção? Abra uma issue e nos ajude a manter esse site em PT-BR para ajudar pessoas como você.