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


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:

SWIFT
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

SWIFT
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.

100%
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

SWIFT
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

SWIFT
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

SWIFT
// ============================================
// 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 leitura
Username 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

SWIFT
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)

SWIFT
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

SWIFT
// ============================================
// 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 leitura
File 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!

SWIFT
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

SWIFT
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

SWIFT
// ============================================
// 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

SWIFT
// ============================================
// 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. O catch { } 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


📝 Exercícios

  1. 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.
  2. 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.
  3. 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.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%