Swift: Tutorial de Tratamento de Erros do Swift
O tratamento de erros é como o airbag de um carro — você espera nunca precisar, mas quando precisa, pode salvar o dia. Esta lição aborda o sistema completo de tratamento de erros do Swift, desde lançar erros até capturá-los com elegância.
1. O Que Você Vai Aprender
- Definir tipos de erro personalizados em conformidade com o protocolo Error
- Declarar funções que podem lançar erros usando throws
- Capturar e tratar diferentes tipos de erro com do-catch
- Converter erros em optionals com try? e ignorar o tratamento com try!
- Use defer para executar a limpeza antes que uma função seja encerrada
2. Uma História Real de uma Desenvolvedora iOS
(1) Problema: Arquivo de configuração ausente — App trava imediatamente
Alice construiu um app de clima que lê um arquivo config.json ao iniciar. Se o arquivo não existir ou tiver um formato ruim, o app trava completamente:
let configPath = "/app/config.json"
let configData = try! String(contentsOfFile: configPath)
let configLines = configData.split(separator: "\n")
O usuário Charlie atualizou o app e ele continuou travando — porque o arquivo de configuração foi acidentalmente excluído durante a atualização. Alice recebeu mais de 50 avaliações de uma estrela: "Abre e trava imediatamente — app lixo!" Sem tratamento de erros, uma única exceção pode arruinar toda a experiência do usuário.
(2) A Solução com do-catch
enum ConfigError: Error {
case fileNotFound
case invalidFormat
}
func loadConfig(path: String) throws -> [String: String] {
guard FileManager.default.fileExists(atPath: path) else {
throw ConfigError.fileNotFound
}
return ["theme": "dark", "units": "metric"]
}
do {
let config = try loadConfig(path: configPath)
print("Config loaded: \(config)")
} catch ConfigError.fileNotFound {
print("Config file not found, using defaults")
}
(3) Resultado: Taxa de falhas de 12% para 0,1%
| Métrica | try! Trava Imediatamente | do-catch Trata |
|---|---|---|
| Config ausente | App trava | Usa padrões |
| Avaliações de 1 estrela | 50+ | 0 |
| Abandono de usuários | 8% | 0,5% |
| Urgência da correção | Deve lançar imediatamente | Pode corrigir na próxima atualização |
3. O Protocolo Error e Lançamento de Erros
O Swift usa o protocolo Error para representar tipos de erro. Qualquer enum, struct ou classe em conformidade com Error pode ser lançada como erro.
graph TB
A[Função] --> B{Erro?}
B -->|Não| C[Retornar valor]
B -->|Sim| D[throw Error]
D --> E[Chamador captura com do-catch]
E --> F[Tratar]
E --> G[Propagar]
E --> H[Converter para Optional]
| Palavra-chave | Finalidade | Exemplo |
|---|---|---|
| Error | Protocolo de erro | enum MeuErro: Error { } |
throw |
Lançar um erro | throw MeuErro.algumCaso |
throws |
Declaração de função | func foo() throws { } |
rethrows |
Closures que podem lançar | func foo(fn: () throws -> Void) rethrows { } |
(1) Definindo Tipos de Erro
enum NetworkError: Error {
case badURL
case timeout(seconds: Int)
case serverError(code: Int)
case noConnection
}
enum ValidationError: Error {
case emptyField(fieldName: String)
case tooShort(minLength: Int)
case invalidFormat(pattern: String)
}
(2) Declarando Funções Lançadoras
enum DivisionError: Error {
case divisionByZero
}
func divide(_ a: Int, by b: Int) throws -> Int {
guard b != 0 else {
throw DivisionError.divisionByZero
}
return a / b
}
do {
let result = try divide(10, by: 0)
print(result)
} catch {
print("Error: \(error)")
}
▶ Exemplo: Validador de Entrada do Usuário
// ============================================
// Validating user input with custom errors
// ============================================
enum ValidationError: Error {
case emptyField(String)
case tooShort(field: String, min: Int)
case invalidEmail
}
func validateRegistration(username: String, email: String, password: String) throws {
guard !username.isEmpty else {
throw ValidationError.emptyField("Username")
}
guard username.count >= 3 else {
throw ValidationError.tooShort(field: "Username", min: 3)
}
guard !email.isEmpty else {
throw ValidationError.emptyField("Email")
}
guard email.contains("@") else {
throw ValidationError.invalidEmail
}
guard password.count >= 6 else {
throw ValidationError.tooShort(field: "Password", min: 6)
}
print("Validation passed!")
}
do {
try validateRegistration(username: "Al", email: "alice@test.com", password: "123")
} catch ValidationError.emptyField(let field) {
print("\(field) cannot be empty")
} catch ValidationError.tooShort(let field, let min) {
print("\(field) must be at least \(min) characters")
} catch ValidationError.invalidEmail {
print("Please enter a valid email")
} catch {
print("Unknown error: \(error)")
}
Saída:
TEXT 📖 Somente leituraUsername must be at least 3 characters
4. do-catch e Tratamento de Erros
do-catch é o mecanismo principal para capturar e tratar erros. Você pode capturar erros específicos, usar correspondência de padrões ou propagar erros para cima.
| Estilo de Captura | Sintaxe | Observações |
|---|---|---|
| Capturar tudo | catch { } | Trata todos os erros |
| Erro específico | catch Erro.especifico { } | Trata apenas aquele erro em particular |
| Correspondência de padrão | catch let erro as MeuErro { } | Conversão de tipo e tratamento |
| Condicional | catch where condicao { } | Trata apenas quando a condição é atendida |
| Propagar | try podeLancar() |
Não trata, continua lançando |
(1) Uso Básico do do-catch
enum FileError: Error {
case notFound, permissionDenied, corrupted
}
func readFile(_ name: String) throws -> String {
if name == "secret.txt" {
throw FileError.permissionDenied
}
return "File content: \(name)"
}
do {
let content = try readFile("secret.txt")
print(content)
} catch FileError.notFound {
print("File not found")
} catch FileError.permissionDenied {
print("Access denied")
} catch {
print("Other error: \(error)")
}
(2) Propagação de Erros (Sem Capturar)
func processFile() throws {
let content = try readFile("data.txt")
print("Processing: \(content)")
}
do {
try processFile()
} catch {
print("Failed to process: \(error)")
}
▶ Exemplo: Leitura e Análise de Arquivo
// ============================================
// Simulated file reading and parsing with error handling
// ============================================
enum FileParseError: Error {
case fileNotFound(String)
case emptyFile
case invalidFormat(line: Int)
}
func parseConfigFile(path: String) throws -> [String: String] {
guard path.hasSuffix(".json") else {
throw FileParseError.fileNotFound(path)
}
let content = "name:Alice\nage:25\ninvalid_line"
let lines = content.split(separator: "\n")
guard !lines.isEmpty else {
throw FileParseError.emptyFile
}
var config: [String: String] = [:]
for (index, line) in lines.enumerated() {
let parts = line.split(separator: ":")
guard parts.count == 2 else {
throw FileParseError.invalidFormat(line: index + 1)
}
config[String(parts[0])] = String(parts[1])
}
return config
}
do {
let config = try parseConfigFile(path: "app.config")
print("Config: \(config)")
} catch FileParseError.fileNotFound(let path) {
print("File not found at: \(path)")
} catch FileParseError.emptyFile {
print("File is empty")
} catch FileParseError.invalidFormat(let line) {
print("Invalid format at line \(line)")
} catch {
print("Unknown error: \(error)")
}
Saída:
TEXT 📖 Somente leituraFile not found at: app.config
5. try?, try! e defer
try? converte erros em optionals. try! afirma que nenhum erro ocorrerá (trava se ocorrer). defer executa código de limpeza quando uma função sai.
| Abordagem | Comportamento | Quando Usar |
|---|---|---|
try |
Requer do-catch | Tratamento padrão de erros |
try? |
Retorna nil em caso de erro | Só se importa com sucesso/fracasso, não com detalhes |
try! |
Trava em caso de erro | Certeza de que não haverá erro (ex.: teste/dados fixos) |
| defer | Executa na saída do escopo | Limpeza de recursos, fechamento de arquivos |
(1) try? e try!
enum ParseError: Error {
case invalidNumber
}
func parseInt(_ text: String) throws -> Int {
guard let num = Int(text) else {
throw ParseError.invalidNumber
}
return num
}
let result1 = try? parseInt("42")
let result2 = try? parseInt("abc")
print("Result 1: \(result1 ?? 0)")
print("Result 2: \(result2 ?? 0)")
let result3 = try! parseInt("100")
print("Result 3: \(result3)")
(2) defer para Limpeza
func processResource() {
print("Step 1: Opening resource")
defer {
print("Step 3: Closing resource (always runs)")
}
print("Step 2: Using resource")
}
processResource()
▶ Exemplo: Tratamento Seguro de Requisições de Rede
// ============================================
// Combining try / try? / try! / defer
// ============================================
enum NetworkError: Error {
case badURL, noData, timeout
}
func fetchData(from urlString: String) throws -> String {
defer {
print(" [cleanup] Closing connection")
}
guard !urlString.isEmpty else {
throw NetworkError.badURL
}
guard urlString.contains(".") else {
throw NetworkError.badURL
}
return "{\"name\": \"Alice\", \"age\": 30}"
}
print("=== try? ===")
if let data = try? fetchData(from: "api.example.com") {
print("Data: \(data)")
} else {
print("Failed to fetch")
}
print()
print("=== do-catch ===")
do {
let data = try fetchData(from: "")
print("Data: \(data)")
} catch NetworkError.badURL {
print("Error: Invalid URL")
} catch {
print("Error: \(error)")
}
print()
print("=== try! ===")
let safeData = try! fetchData(from: "api.example.com")
print("Data: \(safeData)")
Saída:
TEXT 📖 Somente leitura=== try? === [limpeza] Fechando conexão Dados: {"nome": "Alice", "idade": 30} === do-catch === [limpeza] Fechando conexão Erro: URL inválida === try! === [limpeza] Fechando conexão Dados: {"nome": "Alice", "idade": 30}
6. Exemplo Completo: Carregador de Arquivo de Configuração
// ============================================
// Configuration file loader
// Combining Error / throws / do-catch / try? / defer
// ============================================
import Foundation
enum ConfigError: Error {
case fileNotFound(path: String)
case emptyFile
case invalidKeyValue(line: Int, content: String)
case unsupportedKey(String)
}
struct AppConfig {
var theme: String = "light"
var fontSize: Int = 14
var language: String = "en"
var notifications: Bool = true
}
func loadConfig(from path: String) throws -> AppConfig {
defer {
print("[cleanup] Config loader finished")
}
guard FileManager.default.fileExists(atPath: path) else {
throw ConfigError.fileNotFound(path: path)
}
let content = try String(contentsOfFile: path)
let lines = content.split(separator: "\n")
guard !lines.isEmpty else {
throw ConfigError.emptyFile
}
var config = AppConfig()
for (index, line) in lines.enumerated() {
let trimmed = line.trimmingCharacters(in: .whitespaces)
guard !trimmed.isEmpty, !trimmed.hasPrefix("#") else {
continue
}
let parts = trimmed.split(separator: "=", maxSplits: 1).map(String.init)
guard parts.count == 2 else {
throw ConfigError.invalidKeyValue(line: index + 1, content: String(line))
}
let key = parts[0].trimmingCharacters(in: .whitespaces)
let value = parts[1].trimmingCharacters(in: .whitespaces)
switch key {
case "theme": config.theme = value
case "fontSize": config.fontSize = Int(value) ?? config.fontSize
case "language": config.language = value
case "notifications": config.notifications = (value == "true")
default:
throw ConfigError.unsupportedKey(key)
}
}
return config
}
let tempDir = NSTemporaryDirectory()
let testConfigPath = tempDir + "app.config"
let configContent = """
theme=dark
fontSize=16
language=en
notifications=true
"""
try? configContent.write(toFile: testConfigPath, atomically: true, encoding: .utf8)
do {
let config = try loadConfig(from: testConfigPath)
print("=== App Configuration ===")
print("Theme: \(config.theme)")
print("Font Size: \(config.fontSize)")
print("Language: \(config.language)")
print("Notifications: \(config.notifications)")
} catch ConfigError.fileNotFound(let path) {
print("Fatal: Config file not found at \(path)")
print("Using default configuration")
} catch ConfigError.invalidKeyValue(let line, let content) {
print("Error: Invalid format at line \(line): \(content)")
} catch ConfigError.unsupportedKey(let key) {
print("Warning: Unsupported key '\(key)', using default")
} catch {
print("Unexpected error: \(error)")
}
if let fallbackConfig = try? loadConfig(from: tempDir + "nonexistent.config") {
print("\nLoaded fallback config")
} else {
print("\nFallback not found, will use defaults")
}
Saída:
TEXT 📖 Somente leitura[limpeza] Carregador de config finalizado === Configuração do App === Tema: escuro Tamanho da Fonte: 16 Idioma: pt-br Notificações: true [limpeza] Carregador de config finalizado Alternativa não encontrada, serão usados padrões
❓ Perguntas Frequentes
P: Como escolher entre do-catch e try?? R: Use do-catch quando precisar responder de forma diferente a diferentes tipos de erro. Use try? quando só se importar com sucesso/fracasso e não precisar dos detalhes do erro — retorna nil em caso de falha, limpo e simples. P: try! é seguro? R: Não. try! trava quando ocorre um erro — é como dizer ao compilador "Prometo que isso não vai falhar." Só use quando tiver 100% de certeza de que um erro não pode acontecer (ex.: dados de teste fixos). P: Uma função pode lançar múltiplos tipos de erro — como distingui-los no catch? R: Especifique o tipo de erro concreto após o catch, como
catch ErroArquivo.naoEncontrado. Use múltiplas cláusulas catch para tipos diferentes. Ocatch { }final atua como um capturador geral. P: Quando exatamente o defer executa? R: defer executa quando o escopo atual sai, seja normalmente ou lançando um erro. Múltiplos blocos defer executam em ordem inversa de registro (pilha LIFO). P: Qual é a diferença entre throws e rethrows? R: throws significa que a própria função pode lançar. rethrows significa que a função em si não lança, mas seu parâmetro de closure pode. rethrows é comumente usado com funções de ordem superior como map. P: Os tipos de erro devem ser enums? R: Não. Qualquer tipo em conformidade com Error funcionará — enums, structs e classes são todos válidos. Enums são mais comuns porque enumeram claramente todos os casos de erro.
📖 Resumo
- O protocolo Error é a base do tratamento de erros do Swift; normalmente defina tipos de erro como enums
- throws marca uma função que pode lançar; throw realmente lança o erro
- do-catch captura e trata erros, com suporte para tratamento específico por tipo
- try? converte erros em optionals, retornando nil em caso de falha
- try! ignora o tratamento de erros e trava em caso de erro — use com moderação
- defer executa código de limpeza na saída da função, garantindo que os recursos sejam liberados
- Um bom tratamento de erros pode reduzir as taxas de falha de mais de 10% para quase zero
📝 Exercícios
- Iniciante: Defina um enum ErroBanco (com casos saldoInsuficiente, contaInvalida, contaCongelada). Escreva uma função simulada sacar(valor: Double) que lança os erros apropriados e depois trate-os com do-catch.
- Intermediário: Escreva uma função de análise JSON analisarPessoa(json: String) throws -> (nome: String, idade: Int) que trata três casos de erro: string vazia, campo nome ausente e idade não numérica. Chame-a uma vez com try? e uma vez com do-catch.
- Desafio: Construa uma "calculadora de linha de comando". Aceite strings de entrada como "3 + 4", "10 / 0", "abc", analise e avalie-as. Defina um ErroCalculo (com casos expressaoInvalida, divisaoPorZero, operadorDesconhecido) e implemente um fluxo completo de tratamento de erros. Use do-catch para fornecer mensagens de erro amigáveis.