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)"
endTipos comuns de exceção (e quando aparecem)
Algumas exceções aparecem com frequência no dia a dia:
| Exceção | Quando ocorre | Exemplo |
|---|---|---|
ArgumentError | Argumentos inválidos para um método | Integer("12.3") pode gerar ArgumentError |
TypeError | Tipo incompatível em uma operação | "a" + 1 |
NoMethodError | Chamada de método inexistente | nil.upcase |
KeyError | Chave ausente em Hash#fetch | {}.fetch(:x) |
IndexError | Índice inválido em algumas operações | [].fetch(0) |
ZeroDivisionError | Divisão por zero | 10 / 0 |
Errno::ENOENT | Arquivo inexistente | File.read("nao_existe.txt") |
Errno::EACCES | Sem permissão de acesso | File.read("/root/segredo") |
IOError | Falhas de I/O | problemas ao ler/escrever |
Em Ruby, muitas exceções de sistema relacionadas a arquivos ficam sob o namespace Errno::*.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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
end2) 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}"
endRepare 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
endUse 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}"
endNote 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
endO 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}"
endQuando 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.:
NoMethodErrorpornilinesperado) 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
endEvite 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
endSe você realmente precisa capturar “qualquer falha esperada”, liste as classes:
begin
do_something
rescue ArgumentError, TypeError => e
warn e.message
endNã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
end2) Lançar um novo erro com mensagem melhor
begin
File.read(path)
rescue Errno::ENOENT
raise "Config ausente em #{path}. Verifique o caminho."
endEsse 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
endAssim, 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
endBoas 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}"
endErros 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 * 2Se 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
end4) 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...
endWrapper 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
endRescue 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