본문으로 건너뛰기
Previous
Next
Swift 에러 처리 | do-catch, throw, Result

Swift 에러 처리 | do-catch, throw, Result

Swift 에러 처리 | do-catch, throw, Result

이 글의 핵심

enum NetworkError: Error { case badURL case timeout case noConnection case serverError(Int) }.

들어가며

Swift 시리즈 일곱 번째 글이다. 이번엔 실패 가능한 연산이 시그니처·호출부·UI까지 어떻게 이어지는지를 한 덩어리로 묶었고, 실제로 자주 쓰는 패턴부터 보고, 그다음에 문법·개념을 정리하는 순서로 썼다. C++·Java·Objective-C의 예외랑 겉은 비슷해도, Swift는 throws가 붙은 곳에서만 전파가 열리고 호출 쪽은 try로 그 사실을 반드시 인지하게 만든다는 점이 다르다. Error부터 Result, async throws, LocalizedError, NSError 브리징, defer, rethrows, assert·precondition·fatalError까지 프로덕션에서 반복적으로 마주치는 것을 한 흐름에 넣었으니, 목차대로 쭉 읽기보다 “실전 패턴”이 맞는 부분만 골라 봐도 된다.

앱스토어 리젝 직전에 발견한 에러 처리 버그

앱스토어 리젝 직전에 발견한 에러 처리 버그가 있었다. TestFlight·내부 기기에선 잘 돌아갔는데, 심사기기(지역·언어·시간대 조합)에서만 첫 화면 진입 직후 크래시가 났다. 스택을 까보니, 서버 DTO에 없는 필드가 “옛 클라” 경로에선 nil이 아니라 빈 문자열로 내려와 있었고, 그걸 try!로 강제 언랩한 쪽이 터진 거다. DecodingError로 잡을 수 있는 경로를 String이 비어서 실패로 가는 케이스로 두고, 마지막엔 fatalError에 가깝게 묶어 둔 것이 민감한 환경에서만 드러난 셈이었다. 결국 throws·Result로 경로를 갈라 nil/빈 값을 의도한 실패로 돌리고, 앱스토어용 빌드에선 절대 try!로 네트워크/디코딩을 끊지 말자고 팀 룰을 다시 적었다. “리뷰어 환경에선 안 터지겠지”는 통하지 않는다. 여기서 말하려는 건 에러는 정상 흐름이라는 뻔한 말이 아니라, 시그니처에 실패를 안 써 두면 나중에 크래시로 돌아온다는 것이다. 아래 실전 파트는 그때 이후로 내가 코드 리뷰에서 계속 쓰는 패턴을 모아 둔 것이다.

실전 패턴

여기서부터가 실무에서 먼저 쓰는 쪽. 문법이 헷갈리면 뒤쪽 “기본기”로 넘어가도 된다. (이 부분은 그냥 어케 쓰냐가 먼저야.)

네트워킹 에러

HTTP 상태, TLS, DNS, 바디 디코딩을 한 enum으로 합성하거나 계층별로 쪼갠다. 재시도 가능 여부(타임아웃 vs 401)는 케이스·메서드로 표현한다.

enum APIError: Error {
    case transport(Error)
    case status(code: Int, body: String?)
    case decoding(underlying: Error)
}
struct HTTP {
    static func get(_ url: URL) async throws -> String {
        let (data, response) = try await URLSession.shared.data(from: url)
        if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
            let body = String(data: data, encoding: .utf8)
            throw APIError.status(code: http.statusCode, body: body)
        }
        guard let text = String(data: data, encoding: .utf8) else {
            throw APIError.decoding(underlying: CocoaError(.fileReadCorruptFile))
        }
        return text
    }
}

파일 I/O

Data(contentsOf:)·String(contentsOf:)·FileHandle는 Foundation 에러를 던진다. 권한·없는 파일·디스크 풀을 구분하려면 Cocoa/URLError와 매핑을 준비한다. 뒤에 나오는 defer랑 합치면 핸들 누수를 줄이기 쉽다.

func readUTF8File(at url: URL) throws -> String {
    try String(contentsOf: url, encoding: .utf8)
}

JSON 파싱

JSONDecoderDecodingError를 던진다. 필드 수준 메시지를 쓰려면 커스텀 keyDecodingStrategy·init(from:)에서 CustomStringConvertible에 실어 내보내거나, DecodingError를 잡아 도메인 enum으로 변환한다.

struct UserDTO: Decodable { let name: String; let age: Int }
func decodeUser(_ data: Data) throws -> UserDTO {
    do {
        return try JSONDecoder().decode(UserDTO.self, from: data)
    } catch {
        throw AppError.storage("decode: \(error.localizedDescription)")
    }
}

Validation

필드별 실패를 모아서 반환하려면 struct+배열 또는 Result 합성을 쓴다(단일 throw는 첫 실패에서 멈춘다). 폼 UI라면 ValidationError 모음이 편하다.

enum ValidationError: LocalizedError {
    case emptyName
    case invalidAge
    case invalidEmail
    var errorDescription: String? {
        switch self {
        case .emptyName: return "이름은 필수입니다"
        case .invalidAge: return "나이는 0~150 사이여야 합니다"
        case .invalidEmail: return "유효하지 않은 이메일"
        }
    }
}
struct User {
    let name: String, age: Int, email: String
    static func validate(name: String, age: Int, email: String) throws -> User {
        guard !name.isEmpty else { throw ValidationError.emptyName }
        guard (0...150).contains(age) else { throw ValidationError.invalidAge }
        guard email.contains("@") else { throw ValidationError.invalidEmail }
        return User(name: name, age: age, email: email)
    }
}
do {
    _ = try User.validate(name: "", age: 25, email: "[email protected]")
} catch {
    print("검증 실패: \(error.localizedDescription)")
}

Result로 네트워크 경계 정리

async와 함께 쓰면, 호출 측이 await 없이 체이닝하기 좋다.

import Foundation
enum NetworkFailure: Error { case invalidURL, status(Int), decoding }
struct HTTP {
    static func getString(_ urlString: String) async -> Result<String, NetworkFailure> {
        guard let url = URL(string: urlString) else { return .failure(.invalidURL) }
        do {
            let (data, resp) = try await URLSession.shared.data(from: url)
            if let http = resp as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
                return .failure(.status(http.statusCode))
            }
            guard let text = String(data: data, encoding: .utf8) else { return .failure(.decoding) }
            return .success(text)
        } catch { return .failure(.decoding) }
    }
}

async throws vs async -> Result팀 스타일과 테스트 용이성이 기준이 된다. 섞을 거면 get() / Result { try }경계를 한곳에 몰아넣는 편이 덜 꼬인다.

async/await와 에러(실전 쪽 훅)

async 함수는 throws와 같이 쓰면 비동기 전파가 자연스럽다. Task 실패는 Result 래핑이나 withCheckedThrowingContinuation로 브리징할 수 있다.

func loadUser() async throws -> String {
    let s = await HTTP.getString("https://example.com/data.txt")
    switch s {
    case .success(let text): return text
    case .failure(let f):    throw f
    }
}

try await는 “비동기 + 던질 수 있음”을 한 번에 쓰는 말이고, async let으로 병렬화할 땐 한쪽이 실패하면 전부 throw로 묶이는 규칙이 있으니, 부분 실패를 허용하려면 Result로 쪼개거나 TaskGroup에서 개별 catch를 설계하는 식이 필요하다.

에러 처리 개요 — Swift의 철학

throws인가

Swift의 에러 처리는 값을 반환하는 제어 흐름에 가깝다. 런타임이 임의의 위치로 점프하는 “진짜 예외”가 아니라, 컴파일러가 검증하는 계약이다. 호출자는 다음 중 하나를 선택해야 한다: docatch로 처리하거나, 자신의 함수에 throws를 붙여 상위로 넘기거나, try?·try!로 의미를 압축한다. 그래서 “이 함수는 실패할 수 있다”는 정보가 시그니처에 남는다는 것이 1차적인 철학이다.

실패는 타입으로 표현한다

Error를 준수하는 타입(대개 enum)으로 케이스를 나누면, switch와 패턴 매칭으로 누락 없이 대응할 수 있다. String이나 Int 코드만 흘려보내는 것보다 유지보수에 유리하다.

Objective-C·Cocoa와의 연속성

Foundation과 많은 Apple API는 NSError를 쓰며, Swift에서는 Error로 자동 브리징된다. “Swift 네이티브 enum”과 “레거시 도메인 코드”를 경계에서 매핑하는 사고를 갖출 필요가 있다.

Error 프로토콜 — 에러 타입 정의

Error마킹 프로토콜에 가깝다. 별도 요구 사항이 거의 없어서 열거형·구조체·클래스가 모두 채택할 수 있다. 실무에서는 연관 값이 있는 enum이 가장 많이 쓰인다.

enum NetworkError: Error {
    case badURL
    case timeout
    case noConnection
    case serverError(Int)
}
enum ValidationError: Error {
    case emptyField(String)
    case invalidFormat(String)
    case outOfRange(String, min: Int, max: Int)
}

위 선언은 “어떤 종류의 실패가 있는지”를 타입 시스템에 고정한다. 나중에 catch에서 케이스별로 메시지·재시도 정책·로그 레벨을 다르게 줄 수 있다. 연관 값은 맥락(필드 이름, 범위, HTTP 코드 등)을 실어 나르기에 좋다.

throw — 에러 던지기

throw 뒤에는 Error 타입이 와야 한다. 표현식의 타입이 Never에 가깝다고 생각하면 된다(함수는 그 시점에서 정상 반환이 아니라 “에러로 탈출”).

func parsePort(_ s: String) throws -> Int {
    guard let n = Int(s), (1...65535).contains(n) else {
        throw ValidationError.invalidFormat("port=\(s)")
    }
    return n
}

guard와 함께 쓰면 조기 종료가 읽기 쉽다. throw 이후의 코드는 실행되지 않는다.

throws — 에러를 던질 수 있는 함수

함수·이니셜라이저·computed property getter(제한 있음)·클로저 타입에 throws를 붙일 수 있다. throws가 붙은 함수는 “성공 시 T, 실패 시 Error”의 의미로 읽힌다.

func fetchData(from url: String) throws -> String {
    guard !url.isEmpty else { throw NetworkError.badURL }
    if url.contains("timeout") { throw NetworkError.timeout }
    return "데이터"
}

호출부는 try를 써야 하므로, API 문서를 읽지 않아도 시그니처만 보고 실패 가능성을 인지할 수 있다.

docatch — 에러 잡기

do 블록 안에서 try로 호출하고, catch에서 실패를 처리한다. catch는 패턴(아래 절)에 따라 세분화할 수 있다.

do {
    let data = try fetchData(from: "https://example.com")
    print("성공: \(data)")
} catch NetworkError.badURL {
    print("잘못된 URL")
} catch NetworkError.timeout {
    print("타임아웃")
} catch {
    print("기타 에러: \(error)")
}

같은 파일 안의 구체적 케이스(NetworkError.badURL 등)는 타입 전체 이름이 필요하다(추론이 되지 않을 때). 마지막 catchError 전체를 받는 기본 케이스다.

try — 에러 발생 가능한 호출

try는 “이 호출이 에러를 낼 수 있으니, 그에 맞는 맥락(docatch 또는 상위 throws 또는 try?·try!) 안에 있어야 한다”는 표시다.

func caller() throws {
    let data = try fetchData(from: "https://example.com")
    _ = data
}

try표현식 앞에 온다. 중첩 호출 시 각 실패가 어디서 왔는지 catch에서 나누기 어려울 수 있으니, 복잡한 경우에는 중간 값을 let으로 풀어 쓰는 편이 디버깅에 유리하다.

try?Optional로 변환

try?는 실패 시 해당 표현식 전체를 nil로 만든다. 성공이면 옵셔널로 감싼 값이 된다.

let data = try? fetchData(from: "https://example.com")
if let data { print("데이터: \(data)") } else { print("실패") }
let withFallback = (try? fetchData(from: "bad")) ?? "기본값"

에러 정보를 버린다는 점이 트레이드오프다. 로그·알럿·재시도에 필요한 구체 케이스를 잃을 수 있으니, “실패해도 괜찮은 사소한 읽기”에 쓰는 것이 일반적이다.

try! — 강제 언랩(크래시 가능)

try!는 “절대 실패하지 않는다”는 논리적 보증이 있을 때만 써야 한다. 실제로 throw가 발생하면 fatalError와 유사하게 런타임에 종료에 가깝다.

// 앱 기동 시 반드시 존재하는 로컬 리소스만 예시로
let data = try! fetchData(from: "https://example.com")

테스트·개발 전용 코드가 아닌 사용자 환경 프로덕션에서 네트워크·디스크에 try!를 쓰는 것은 권장되지 않는다. 실패는 정상 흐름이 될 수 있기 때문이다. 위 “앱스토어” 이야기가 바로 이 규칙을 어겼을 때 벌어지는 쪽이다.

catch 패턴 — 여러 catch 블록

catchis, as?, where, 연관 값 패턴을 사용할 수 있다. 한 블록에서 enum 케이스를 세밀하게 나누거나, NSErrordomain·code에 의존하는 Cocoa 코드에 맞출 수 있다.

do {
    _ = try parsePort("99999")
} catch let err as NetworkError {
    print("NetworkError: \(err)")
} catch {
    if let e = error as? LocalizedError, let m = e.errorDescription {
        print(m)
    } else {
        print(error.localizedDescription)
    }
}

라이브러리의 non-frozen public enum은 이후 케이스가 추가될 수 있으므로, switch 대신 catch@unknown default에 해당하는 방어(catch { ... } 마지막 구간)를 두는 습관이 좋다.

에러 전파(Propagating errors)

throws 함수 안에서는 try로 호출한 에러를 잡지 않고 그대로 밖으로 보낼 수 있다. 스택 풀기(wind)는 Swift 런타임이 맡는다.

func loadAndParse() throws -> [String: Any] {
    let raw = try fetchData(from: "https://example.com")
    // 가정: Foundation JSON
    let data = Data(raw.utf8)
    let obj = try JSONSerialization.jsonObject(with: data) as? [String: Any]
    guard let obj else { throw NetworkError.serverError(500) }
    return obj
}

중첩이 깊어지면 Result로 변환하거나, async계층으로 옮겨 async throws로 단순화하는 경우가 있다.

Result<Success, Failure> 타입

Result는 성공·실패를 으로 담는 열거형이다. Failure: Error로 제한된다. throws와 달리 “함수가 아닌 곳(프로퍼티, 제네릭, 클로저 저장 필드)에 실패”를 담을 때 유리하다.

func fetchData(from url: String) -> Result<String, NetworkError> {
    guard !url.isEmpty else { return .failure(.badURL) }
    if url.contains("timeout") { return .failure(.timeout) }
    return .success("데이터")
}
switch fetchData(from: "https://example.com") {
case .success(let data): print("성공: \(data)")
case .failure(let error): print("실패: \(error)")
}

map·mapError·flatMap으로 합성할 수 있어, 동기 체이닝 스타일에 잘 맞는다.

let result = fetchData(from: "https://example.com")
    .map { $0.uppercased() }
    .mapError { _ in NetworkError.noConnection }

Resultthrows 변환은 아래처럼 이어 붙이면 된다.

func getData() throws -> String {
    let result = fetchData(from: "https://example.com")
    return try result.get() // 실패 시 throw
}
func getDataResult() -> Result<String, Error> {
    Result { try loadAndParse() } // Swift 5 이상
}

Result.get()이 내부에서 throws를 일으킨다는 점이 연결 고리다.

defer — 정리 코드

defer 블록은 함수가 return하든 throw하든 가장 나중에 역순으로 실행된다. 파일 핸들, 락, 임시 버퍼 정리에 쓰인다.

func writeTempFile(content: String) throws -> URL {
    let url = FileManager.default.temporaryDirectory.appendingPathComponent("t.txt")
    let fh = try FileHandle(forWritingTo: url)
    defer { try? fh.close() }
    if let d = content.data(using: .utf8) {
        try fh.write(contentsOf: d)
    }
    return url
}

defer 안에서 throw는 가능하지만, 제어 흐름이 복잡해질 수 있어 가급적 정리용으로만 쓰는 것이 안전하다.

rethrows — 고차 함수

rethrows는 “인자로 받은 클로저가 throw할 때만, 나도 throw한다”는 뜻이다. map·forEach류의 표준 라이브러리 패턴과 같다.

func mapEach<T, U>(
    _ items: [T],
    _ body: (T) throws -> U
) rethrows -> [U] {
    var out: [U] = []
    for x in items { out.append(try body(x)) }
    return out
}

rethrows 함수는, 실제로 인자 클로저가 throw하지 않는 한 호출부에 try를 요구하지 않는다(컴파일러가 조건부로 처리). 직접 throw를 섞는 API를 설계할 때 유용하다.

커스텀 에러 — enum으로 Error 구현

엔트로피를 줄이기 위해 도메인 단위로 enum을 쪼개는 것이 일반적이다(네트워크, 검증, 저장소 등). LocalizedError는 사용자 메시지에만 쓰고, 내부 식별enum 케이스로 남긴다.

enum AppError: Error {
    case network(NetworkError)
    case validation(ValidationError)
    case storage(String) // 키체인, 파일 등
}

이렇게 중첩하면 상위 catch에서 큰 갈래를 나누고, 내부에서 세부 케이스로 들어갈 수 있다.

LocalizedError — 사용자 친화적 메시지

LocalizedErrorerrorDescription 등을 제공해 UI에 표시할 문자열을 일관되게 쓰게 돕는다.

enum FileError: LocalizedError {
    case notFound
    case permissionDenied
    var errorDescription: String? {
        switch self {
        case .notFound: return "파일을 찾을 수 없습니다"
        case .permissionDenied: return "권한이 없습니다"
        }
    }
}

LocalizedError를 구현하지 않으면 String(describing: error) 류에 내부 식별자가 섞일 수 있으니, 앱이 노출하는 모든 사용자 대면 경로는 구현해 두는 것이 좋다. failureReason·recoverySuggestion·helpAnchor는 지원팀·접근성·앱 스토어 심사 응답에도 쓰인다.

NSError 브리지 — Cocoa·Foundation 에러

Objective-C API는 NSError ** 로 에러를 돌려주며, Swift에서는 throws로 매핑되거나 Error로 온다. NSError로 캐스팅해 domain·code·userInfo를 읽을 수 있다.

import Foundation
func nsExample() {
    do {
        try "a".write(toFile: "/invalid\0path", atomically: true, encoding: .utf8)
    } catch {
        let e = error as NSError
        print("domain: \(e.domain) code: \(e.code)")
        if let s = e.userInfo[NSLocalizedDescriptionKey] {
            print("info: \(s)")
        }
    }
}

도메인 상수(NSURLErrorDomain 등)와 코드는 애플 문서에서 대조해 원인을 좁힌다. Swift 전용 enum으로 변환(map 함수 하나)해두면 UI·로그 계층이 단순해진다.

assertprecondition — 디버깅 vs 프로덕션

assert(_:_:file:line:)는 릴리스 빌드에서 no-op이 될 수 있어(최적화), 개발 중 불변식 검사에 쓴다. precondition(_:_:file:line:)는 유지하는 빌드에서 위반 시 치명적 실패에 가깝다(용도·빌드 설정에 따라 다름). 일반 API 계약·사용자 입력은 throwsResult 반환하는 편이 낫다. “이건 정말 버그일 때만” assert/precondition을 쓰는 게 맞다.

func indexForSorted(_ a: [Int], value: Int) -> Int {
    precondition(a == a.sorted(), "정렬되지 않은 배열")
    // ...
    return 0
}

fatalError — 복구 불가

fatalError는 프로세스를 중단시킨다. 복구가 불가능하거나, 계속되면 데이터가 손상되는 경로에만 쓴다(예: 필수 구성 누락, 개발용 스텁). 라이브러리 공개 API에서 남용하면 앱이 죽는다. 가능하면 throws·Result·로그 후 우아한 퇴장으로 바꾼다.

func requiredKey() -> String {
    guard let v = ProcessInfo.processInfo.environment["REQUIRED"] else {
        fatalError("REQUIRED 누락: 개발/배포 구성을 확인하세요")
    }
    return v
}

async/await와 에러 — 기본(참고)

async 함수는 throws와 함께 쓰일 수 있으며, 비동기 전파가 자연스럽다. 위 “실전 패턴” 쪽에 이미 try await·Result로 네트워크 끊는 예는 넣어 두었고, 여기서는 문법 수준만 다시 짚는다. Task의 실패는 Result 래핑이나 withCheckedThrowingContinuation로 직접 옮길 수 있으며, 동시에 여러 asyncasync let으로 병렬화할 때는 한쪽의 에러가 전체 throw로 전파될 수 있으니, 부분 실패를 허용하려면 Result로 쪼개거나 TaskGroup에서 개별 catch를 설계하는 편이 안전하다.

후회하지 않는 에러 처리

여기는 팀 룰 말고 나 혼자 정해 둔 가이드다. 딱딱하게 읽힐 수 있는데, 실제로는 PR에서 이렇게 말하곤 해.

실패할 수 있으면 시그니처에 throwsResult로 박아 둔다. Bool만 던지고 “실패 이유는 주석/슬랙” 같은 건 나중에 를 괴롭힌다. try?는 “실패 = 그냥 없음”으로도 되는 옵셔널 경로에만 쓰고, 로그·알럿·재시도가 필요한 데엔 쓰지 말자. try!는 번들 안 리소스처럼 정말 보증할 수 있을 때만 쓰고, 네트워크·유저 입력·서버 DTO엔 쓰지 말기 — 쓰면 크래시는 내가 책임진다.

LocalizedError는 사용자가 볼 수 있는 말로만 채우고, 스택·내부 id는 로그·크래시 리포터로 보낸다. Cocoa 쪽 NSErrordomain+code+userInfo한 세트로 읽는다. 도메인만 외우면 헷갈린다. assert·precondition은 “이게 깨지면 곧장 버그”일 때, 일반 API 실패엔 throws 써. defer는 락·핸들 풀기용으로만 쓰고, 거기서 또 던지면 제어 흐름이 미쳐. rethrows는 고차에서만, 함수마다 throws 붙이지 말기. I/O·외부는 async 쪽이 맞고, CPU만 맴도는 건 그에 맞게 나눔.

말투 풀자면, 에러는 숨기지 말고 타입이나 시그니처에 남기고, 크래시로 도망가지 말자 — 이게 전부다. (반대로 enum 예쁘게 100개 만든 뒤 아무도 catch 안 쓰면? 그거 그냥 쓰레기 맞지. 팀이 읽을 만큼만 정리하자고.)


일반적 문제 — 이렇게 풀었다

표는 없다. 자주 겪는 순으로 달아 둠.

try를 안 붙인다는 컴파일 에러는, throws 함수를 docatch·상위 throws·try?·try! 없이 쓴 경우다. error 케이스를 못 잡는 느낌이면, Error는 열거형이 아니라서 as?로 풀거나 우리 쪽 enum으로 감싼 뒤 catch에 맞춘다. catch에 케이스를 다 못 쓰겠다 싶으면 enum을 중첩해서 위에서 큰 갈래만 먼저 잡는다. Result끼리 map이 안 붙는 건 Failure 타입이 달라서이니 mapError로 통일하거나 Error로 승격한다. async let로 병렬 뛰다 한쪽만 터지면 전부 throw로 묶이니까, 부분 실패를 모을 땐 작업마다 Result를 쌓는 식이 낫다. 로그에 케이스 이름만 있고 사용자 메시지가 없다면 LocalizedError·도메인 매핑이 빠진 것이다. NSError는 코드만 보면 애매할 수 있으니 domain까지 같이 본다. defer가 이상하면 중첩을 줄이고, 획득–해제 쌍이 한 함수 안에만 있게 맞춘다. try! 크래시는 환경(경로, 서버)이 변해서 그렇다 — throws로 돌리고, 사용자에겐 로 돌려준다. 권하는 디버깅 순서는 이렇다. (1) 최소로 다시 띄우고, (2) catch에서 String(reflecting: error) / NSError로 성격을 잡고, (3) 네트워크·파일의 재현 조건을 고정한 다음, (4) LocalizedError·로그·메트릭이 같이 말하는지 본다. 이 순서 꼬이면? 그냥 한 잔 하고 (5) 누가 try!를 넣었는지 git blame.


내부 동작을 한눈에(개념도)

flowchart TD
  A[입력·이벤트] --> B[검증·디코딩]
  B --> C[핵심 연산]
  C --> D[부작용: 네트워크·파일]
  D --> E[성공 반환]
  B -. 실패 .-> F[Error]
  C -. 실패 .-> F
  D -. 실패 .-> F
  F --> G[ catch / mapError / async throw ]

핵심 정리

  1. Error: 실패 종류를 타입으로 고정한다.
  2. throw / throws: 제어흐름이 아니라 서명이 있는 실패 전파.
  3. try / try? / try!: 전파·압축·치명 — 의도를 드러낸다.
  4. docatch: 케이스·연관 값·Cocoa에 맞는 처리.
  5. Result: 실패를 으로 합성할 때.
  6. defer / rethrows / async throws: 자원·고차·동시성과 어울리는 완성도.

다음에 읽을 글

관련 글(다른 언어·주제)

자주 묻는 질문 (FAQ) — 본문 보강

Swift의 throws는 C++/Java의 예외와 같나요? 문법은 비슷하지만, “모든 곳에서 잡힐 수 있는 예외”가 아니라 시그니처에 명시된 실패 전파에 가깝습니다. unchecked 느낌이 덜합니다. Result를 쓰면 throws가 필요 없나요? 아닙니다. 역할이 다릅니다. 동기 경계·클로저 저장·콤비네이터 합성에는 Result, 일반 앱 흐름·async와의 통일에는 throws가 자연스럽습니다. 중간에 get() / Result { try }로 이어 주면 됩니다. Cocoa NSError는 어디서 잡나요? 대부분 Error로 보이고, catch에서 as NSError로 풀 수 있습니다. 도메인/코드 표를 애플 문서와 대조하세요. 팀에서 try!를 금지할까요? 프로덕션 경로에선 지양·린트 규칙을 두는 팀이 많습니다. 예외(번들 리소스)는 PR에서 근거를 남깁니다. async에서 부분 실패를 모으려면? TaskGroup+Result, 또는 async let을 쓰지 않고 작업마다 Result를 모으는 식이 안전할 때가 있습니다.

이 글의 키워드

Swift, 에러처리, Error, Result, throws, try, do-catch, LocalizedError, NSError, async throws, defer, rethrows로 검색하면 이 글과 맞닿는 주제입니다.