Ruby do Zero: Tratamento de Exceções — begin/rescue/ensure e Erros Comuns

Capítulo 19

Tempo estimado de leitura: 9 minutos

+ Exercício

O que é uma exceção e por que ela existe

Uma exceção é um objeto que representa um erro (ou uma condição inesperada) ocorrido durante a execução do programa. Em Ruby, quando algo dá errado, o código pode lançar (raise) uma exceção. Se ninguém a tratar, o programa é interrompido e Ruby imprime uma mensagem e um backtrace (rastreamento de chamadas).

Tratar exceções serve para: (1) transformar falhas em respostas previsíveis (mensagens, valores padrão, tentativas de recuperação), (2) adicionar contexto útil ao erro, (3) garantir limpeza de recursos (fechar arquivos, desfazer locks) mesmo quando algo falha.

Estrutura do begin/rescue/else/ensure

O bloco begin define uma região onde exceções podem ser capturadas. Você pode ter um ou mais rescue, um else (executa apenas se não houve exceção) e um ensure (executa sempre, com ou sem exceção).

begin
  # código que pode falhar
rescue TipoDeErro => e
  # tratamento
else
  # executa se NÃO houve exceção
ensure
  # executa SEMPRE
end
  • rescue: captura exceções específicas (recomendado) ou genéricas (com cuidado).
  • else: útil para separar “caminho feliz” do tratamento, sem duplicar lógica.
  • ensure: ideal para liberar recursos (fechar arquivo, remover arquivo temporário, restaurar estado).

Exemplo mínimo com else e ensure

begin
  result = 10 / divisor
rescue ZeroDivisionError
  result = nil
else
  puts "Divisão OK"
ensure
  puts "Fim do bloco (sempre roda)"
end

Tipos comuns de exceção (e quando aparecem)

Algumas exceções aparecem com frequência no dia a dia:

ExceçãoQuando ocorreExemplo
ArgumentErrorArgumentos inválidos para um métodoInteger("12.3") pode gerar ArgumentError
TypeErrorTipo incompatível em uma operação"a" + 1
NoMethodErrorChamada de método inexistentenil.upcase
KeyErrorChave ausente em Hash#fetch{}.fetch(:x)
IndexErrorÍndice inválido em algumas operações[].fetch(0)
ZeroDivisionErrorDivisão por zero10 / 0
Errno::ENOENTArquivo inexistenteFile.read("nao_existe.txt")
Errno::EACCESSem permissão de acessoFile.read("/root/segredo")
IOErrorFalhas de I/Oproblemas ao ler/escrever

Em Ruby, muitas exceções de sistema relacionadas a arquivos ficam sob o namespace Errno::*.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

Passo a passo: validação de entrada com conversões seguras

Conversões de texto para número são uma fonte comum de exceções. Um padrão limpo é: (1) converter, (2) validar regras de negócio, (3) lançar um erro claro quando inválido, (4) tratar apenas onde faz sentido (por exemplo, na borda do sistema: CLI, controller, job).

1) Converter e validar dentro de um método

def parse_id(input)
  id = Integer(input) # pode gerar ArgumentError
  raise ArgumentError, "id deve ser positivo" if id <= 0
  id
end

2) Tratar na borda (onde você decide a resposta ao usuário)

begin
  id = parse_id(user_input)
  puts "ID válido: #{id}"
rescue ArgumentError => e
  puts "Entrada inválida: #{e.message}"
end

Repare que o método parse_id não imprime nada e não “engole” o erro. Ele apenas garante uma regra e comunica falhas via exceção. Isso mantém o código previsível e testável.

Alternativa: retornar nil em vez de exceção (quando faz sentido)

Às vezes, uma falha de conversão é esperada e você prefere um fluxo sem exceções. Um padrão é criar um método “try”:

def try_integer(input)
  Integer(input)
rescue ArgumentError, TypeError
  nil
end

Use esse estilo quando “não conseguir converter” for um caso normal do domínio. Se for um erro que deve interromper o fluxo, prefira lançar exceção com mensagem clara.

Operações com arquivos: tratamento correto e ensure

Arquivos falham por motivos comuns: caminho errado, permissão, arquivo em uso, encoding inesperado. Trate apenas o que você consegue resolver ou transformar em resposta útil.

Leitura simples com tratamento específico

def read_config(path)
  File.read(path)
rescue Errno::ENOENT
  raise "Arquivo de configuração não encontrado: #{path}"
rescue Errno::EACCES
  raise "Sem permissão para ler: #{path}"
end

Note que aqui estamos relançando (raise) com uma mensagem mais amigável. Isso adiciona contexto. Se você quiser preservar o tipo original e o backtrace, pode relançar o mesmo erro ou encadear a causa (ver seção de encadeamento).

Garantindo fechamento com ensure (quando não usar bloco)

Ruby permite abrir arquivo com bloco (File.open(...){...}), que já fecha automaticamente. Mas quando você precisa manter o handle em uma variável, ensure é a ferramenta:

file = nil
begin
  file = File.open(path, "r")
  data = file.read
  process(data)
rescue Errno::ENOENT => e
  warn "Não foi possível abrir: #{e.message}"
ensure
  file.close if file
end

O ensure roda mesmo se process falhar no meio.

Escrita com criação de diretório e erros previsíveis

def write_report(path, content)
  File.write(path, content)
rescue Errno::ENOENT
  raise "Diretório inexistente para salvar o relatório: #{path}"
rescue Errno::EACCES
  raise "Sem permissão para escrever em: #{path}"
end

Quando tratar vs quando deixar o erro subir

Uma regra prática: trate exceções onde você consegue tomar uma decisão útil. Caso contrário, deixe o erro subir para um nível que saiba o que fazer (ex.: camada de interface, job runner, ponto de entrada do script).

Trate quando:

  • Você consegue oferecer um fallback seguro (ex.: usar valor padrão, tentar outro caminho).
  • Você precisa transformar o erro em uma mensagem para o usuário.
  • Você precisa adicionar contexto (qual arquivo, qual id, qual operação).
  • Você precisa garantir limpeza de recursos (ensure).

Deixe subir quando:

  • O erro indica bug (ex.: NoMethodError por nil inesperado) e você não tem como recuperar corretamente.
  • Tratar exigiria “adivinhar” o estado correto, mascarando problemas.
  • O chamador é quem deve decidir (ex.: biblioteca não deve imprimir nem encerrar o programa).

Resgatar exceções: especificidade e armadilhas

Resgatar exceções específicas

Prefira capturar tipos específicos. Isso evita esconder erros não relacionados.

begin
  total = Integer(input)
rescue ArgumentError
  total = 0
end

Evite rescue genérico sem critério

rescue sem especificar classe captura StandardError (não captura tudo). Ainda assim, pode esconder bugs.

begin
  do_something
rescue
  # ruim: pode esconder falhas reais
end

Se você realmente precisa capturar “qualquer falha esperada”, liste as classes:

begin
  do_something
rescue ArgumentError, TypeError => e
  warn e.message
end

Não use exceções para fluxo normal sem necessidade

Exceções são ótimas para situações excepcionais. Se um caso é comum (por exemplo, “campo opcional vazio”), prefira validações explícitas e retornos previsíveis.

Relançar (propagar) e adicionar contexto

Você pode capturar um erro para adicionar contexto e relançar. Há três padrões comuns:

1) Relançar o mesmo erro (preserva tipo e backtrace)

begin
  File.read(path)
rescue Errno::ENOENT
  warn "Falhou ao ler #{path}"
  raise
end

2) Lançar um novo erro com mensagem melhor

begin
  File.read(path)
rescue Errno::ENOENT
  raise "Config ausente em #{path}. Verifique o caminho."
end

Esse padrão simplifica a mensagem, mas troca o tipo da exceção (agora é RuntimeError se você não especificar).

3) Encadear a causa (mensagem + erro original)

Para manter a causa original acessível, você pode encadear usando raise NovoErro, msg, cause: e (Ruby moderno):

class ConfigError < StandardError; end

def load_config(path)
  File.read(path)
rescue Errno::ENOENT => e
  raise ConfigError, "Config não encontrada: #{path}", cause: e
end

Assim, quem depura consegue ver a exceção de alto nível e a causa raiz.

Criando exceções customizadas (e mensagens úteis)

Exceções customizadas ajudam a comunicar erros de domínio (regras do seu problema) sem misturar com erros técnicos. Em geral, herde de StandardError.

class ValidationError < StandardError; end

def validate_email!(email)
  if email.nil? || email.strip.empty?
    raise ValidationError, "email é obrigatório"
  end

  unless email.include?("@")
    raise ValidationError, "email inválido: deve conter @"
  end

  true
end

Boas mensagens: diga o que falhou e por quê, e quando útil inclua o valor (com cuidado para não vazar dados sensíveis).

Passo a passo: usando exceção de domínio no fluxo

begin
  validate_email!(email)
  save_user(email)
rescue ValidationError => e
  puts "Não foi possível salvar: #{e.message}"
end

Erros comuns ao usar begin/rescue/ensure

1) Colocar código demais dentro do begin

Quanto maior o bloco, mais difícil saber o que realmente pode falhar e o que você está capturando. Prefira blocos pequenos e focados:

# melhor: begin só no trecho crítico
begin
  data = File.read(path)
rescue Errno::ENOENT
  data = ""
end
process(data)

2) Engolir erro e seguir com estado inválido

begin
  price = Float(input)
rescue ArgumentError
  # ruim: price fica nil e pode quebrar depois
end
puts price * 2

Se você tratar, defina um valor padrão seguro ou interrompa o fluxo com uma mensagem clara.

3) Usar ensure para mudar lógica de retorno

ensure deve ser para limpeza. Evite retornar de dentro do ensure, pois pode mascarar exceções e confundir o fluxo.

def example
  begin
    raise "falhou"
  ensure
    return 123 # ruim: esconde a exceção
  end
end

4) Resgatar StandardError e imprimir, sem relançar, em código de biblioteca

Se seu código é chamado por outros, imprimir e seguir pode tornar o sistema imprevisível. Em bibliotecas, prefira relançar com contexto ou retornar um resultado explícito (dependendo do design).

Padrões úteis para manter o código limpo

Guard clauses + exceções de domínio

class PaymentError < StandardError; end

def charge!(amount)
  raise PaymentError, "valor deve ser > 0" unless amount > 0
  # chama gateway...
end

Wrapper de operação com tratamento centralizado

Quando várias operações parecidas falham do mesmo jeito, crie um método que encapsula o padrão:

def with_file_read(path)
  File.read(path)
rescue Errno::ENOENT => e
  raise "Arquivo não encontrado: #{path}", cause: e
rescue Errno::EACCES => e
  raise "Sem permissão: #{path}", cause: e
end

Rescue em métodos (forma curta)

Ruby permite rescue no fim do método para casos simples, mas use com parcimônia para não esconder complexidade:

def safe_float(input)
  Float(input)
rescue ArgumentError, TypeError
  nil
end

Agora responda o exercício sobre o conteúdo:

Em Ruby, qual é o papel do ensure em um bloco begin/rescue/else/ensure?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

O ensure roda independentemente de ter ocorrido exceção. Por isso é ideal para tarefas de limpeza e liberação de recursos (por exemplo, fechar um arquivo), garantindo consistência mesmo quando algo falha.

Próximo capitúlo

Ruby do Zero: Leitura e Escrita de Arquivos — File, IO e Boas Práticas

Arrow Right Icon
Capa do Ebook gratuito Ruby do Zero: Fundamentos, Coleções, Blocos e Organização de Código
90%

Ruby do Zero: Fundamentos, Coleções, Blocos e Organização de Código

Novo curso

21 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.