Dominando Exceptions em Ruby: Guia Completo para Tratamento de Erros
Até agora, desenvolvemos código em um mundo perfeito onde nada jamais dá errado. Toda chamada de biblioteca funciona, usuários nunca inserem dados incorretos e os recursos são abundantes. Bem, isso está prestes a mudar. No mundo real, erros acontecem, e bons programas (e programadores) os antecipam e se organizam para tratá-los elegantemente.
O Problema dos Códigos de Retorno
Uma abordagem tradicional é usar códigos de retorno para sinalizar erros. Por exemplo, o método File.open
poderia retornar algum valor específico para indicar falha. O problema dessa abordagem é que gerenciar todos esses códigos de erro pode ser uma dor de cabeça. Se uma função chama open
, depois read
e finalmente close
, e cada uma pode retornar uma indicação de erro, como a função pode distinguir esses códigos de erro no valor que retorna ao seu chamador?
Ruby usa exceptions para ajudar a resolver o problema de responder a erros. Exceptions permitem que você empacote informações sobre um erro em um objeto. Esse objeto de exception é então propagado automaticamente de volta pela pilha de chamadas até que o sistema de runtime encontre código que declare explicitamente que sabe como lidar com esse tipo de exception.
A Classe Exception e sua Hierarquia
Informações sobre uma exception são encapsuladas em um objeto da classe Exception
ou em uma de suas classes filhas. Ruby predefine uma hierarquia organizada de exceptions. A subclasse mais importante de Exception
é StandardError
. A exception StandardError
e suas subclasses representam as condições excepcionais que você vai querer capturar em seu código.
Quando você precisa criar uma exception personalizada, suas classes de exception devem ser subclasses de StandardError
ou uma de suas filhas. Frequentemente, o único dado novo associado a uma exception personalizada é que ela é uma exception personalizada, então você pode declará-la em uma linha:
class UsuarioAusenteError < StandardError; end
Ponto e vírgula, que são raros em Ruby, são usados para separar expressões quando você coloca mais de uma em uma linha. Por convenção, nomes de classes de exception personalizadas terminam com Error
.
Tratando Exceptions
Vamos ver um exemplo de código que baixa o conteúdo de uma página web e o escreve em um arquivo, linha por linha:
require "open-uri"
URI.open("https://pragprog.com/news/index.html") do |pagina_web|
saida = File.open("index.html", "w")
while (linha = pagina_web.gets)
saida.puts linha
end
saida.close
end
O que acontece se ocorrer um erro fatal no meio do processo? Certamente não queremos armazenar uma página incompleta no arquivo de saída. Vamos adicionar código de tratamento de exceptions:
require "open-uri"
nome_arquivo = "index.html"
URI.open("https://pragprog.com/news/#{nome_arquivo}") do |pagina_web|
saida = File.open(nome_arquivo, "w")
begin
while (linha = pagina_web.gets)
saida.puts linha
end
saida.close
rescue StandardError
$stderr.warn "Falha ao baixar #{nome_arquivo}: #{$!}"
saida.close
File.delete(nome_arquivo)
raise
end
end
Múltiplas Cláusulas Rescue
Você pode ter múltiplas cláusulas rescue em um método ou bloco begin, e cada cláusula rescue pode especificar múltiplas exceptions para capturar. No final de cada cláusula rescue, você pode dar ao Ruby o nome de uma variável local para receber a exception correspondida:
begin
eval string_codigo
rescue SyntaxError, NameError => e
print "String não compila: " + e.to_s
rescue StandardError => e
print "Erro executando script: " + e.to_s
end
Garantindo Limpeza com Ensure
Às vezes você precisa garantir que um processamento específico seja feito no final de um bloco de código, independentemente de uma exception ter sido levantada. A cláusula ensure
faz exatamente isso. Uma cláusula ensure vai após a última cláusula rescue e sempre será executada:
arquivo = File.open("arquivo_teste")
begin
# .. processar
rescue
# .. tratar erro
ensure
arquivo.close
end
A cláusula else
é uma construção similar, embora menos útil. Se presente, vai após as cláusulas rescue e antes de qualquer ensure. O corpo de uma cláusula else é executado apenas se nenhuma exception for levantada pelo corpo principal do código:
arquivo = File.open("arquivo_teste")
begin
# .. processar
rescue
# .. tratar erro
else
puts "Parabéns-- nenhum erro!"
ensure
arquivo.close
end
Tentando Novamente com Retry
Às vezes você pode ser capaz de corrigir a causa de uma exception. Nesses casos, você pode usar a instrução retry
dentro de uma cláusula rescue para repetir todo o bloco begin/end. Claramente, existe um tremendo potencial para loops infinitos aqui, então este é um recurso para usar com cuidado:
tentativas = 0
begin
tentativas += 1
@conexao = @servidor_remoto.ler_dados
rescue TimeOutError
if @servidor_remoto && tentativas < 10 then
sleep(tentativas ** 2)
retry
else
raise
end
end
Este código tenta ler dados do servidor_remoto. Se retornar um TimeOutError e se o servidor_remoto existir, o código dorme por um tempo e tenta novamente. Ele acompanha o número de tentativas, aumentando o timeout, até que eventualmente, se o número de tentativas ficar muito alto, pare de tentar se conectar e apenas levante o erro.
Levantando Exceptions
Você pode levantar exceptions em seu código com o método raise
(ou seu sinônimo menos usado, fail
). Existem várias formas de usar raise
:
raise # re-levanta a exception atual
raise "codificação mp3 ruim" # cria RuntimeError
raise InterfaceException, "Falha no teclado" # exception específica
Aqui estão alguns exemplos típicos de raise
em ação:
raise
raise "Nome ausente" if nome.nil?
if i >= nomes.size
raise IndexError, "#{i} >= tamanho (#{nomes.size})"
end
raise ArgumentError, "Nome muito grande", caller
Adicionando Informações às Exceptions
Você pode definir suas próprias exceptions para armazenar qualquer informação necessária para transmitir do local do erro. Por exemplo, certos tipos de erros de rede podem ser temporários dependendo das circunstâncias:
class TentarNovamenteException < RuntimeError
attr_reader :ok_para_tentar_novamente
def initialize(ok_para_tentar_novamente)
@ok_para_tentar_novamente = ok_para_tentar_novamente
end
end
# Uso da exception personalizada
def ler_dados(contador_tentativas)
dados = @socket.read(512)
if dados.nil?
raise TentarNovamenteException.new(contador_tentativas < 10),
"erro temporário de leitura"
end
# .. processamento normal
end
# Tratamento da exception
tentativas = 0
begin
tentativas += 1
@conexao = @servidor_remoto.ler_dados(tentativas)
rescue TentarNovamenteException => e
retry if e.ok_para_tentar_novamente
raise
end
Usando Catch e Throw
Embora o mecanismo de exception de raise
e rescue
seja ótimo para abandonar a execução quando as coisas dão errado, às vezes é útil poder saltar de uma construção profundamente aninhada durante o processamento normal. É aqui que os raramente usados catch
e throw
são úteis.
lista_palavras = File.open("lista_palavras")
catch(:concluido) do
resultado = []
while (linha = lista_palavras.gets)
palavra = linha.chomp
throw :concluido unless /^\w+$/.match?(palavra)
resultado << palavra
end
puts resultado.reverse
end
catch
define um bloco rotulado com o nome dado. O bloco é executado normalmente até que um throw
seja encontrado. Quando Ruby encontra um throw
, ele volta pela pilha de chamadas procurando um bloco catch com um símbolo correspondente.
Se o throw
for chamado com um segundo parâmetro opcional, esse valor é retornado como o valor do catch
:
def perguntar_e_obter(pergunta)
print pergunta
resposta = readline.chomp
throw :saida_solicitada if resposta == "!"
resposta
end
catch :saida_solicitada do
nome = perguntar_e_obter("Nome: ")
idade = perguntar_e_obter("Idade: ")
sexo = perguntar_e_obter("Sexo: ")
# processar informações
end
Boas Práticas para Tratamento de Exceptions
Para finalizar este guia completo sobre exceptions em Ruby, aqui estão algumas boas práticas essenciais:
1. Sempre capture StandardError ou suas subclasses - Nunca capture Exception diretamente, pois isso pode mascarar erros críticos do sistema.
2. Use ensure para limpeza obrigatória - Recursos como arquivos e conexões de rede devem sempre ser fechados no bloco ensure.
3. Seja específico com rescue - Capture apenas as exceptions que você sabe como tratar adequadamente.
4. Crie exceptions personalizadas quando apropriado - Elas tornam seu código mais expressivo e facilitam o tratamento específico de erros.
5. Use retry com parcimônia - Sempre implemente um contador para evitar loops infinitos.
Com essas técnicas, você estará preparado para criar aplicações Ruby robustas e resilientes, capazes de lidar elegantemente com situações excepcionais que inevitavelmente surgem no desenvolvimento de software real.
Nenhum comentário:
Postar um comentário