Lesson 87Error Handling

Failable Initializers

When Initialization Can Fail

Sometimes creating an instance should fail if the input is invalid. Failable initializers (init?) return an optional - either an instance or nil.

Syntax

init?(parameters) {
    if invalid { return nil }
    // Initialize properties
}

init? vs init throws

init? (Failable)

Returns nil on failure

Simple pass/fail cases

init throws (Throwing)

Throws specific error

Need error details

Common Use Cases

  • Input validation - Age, email, URL format
  • Raw value enums - Auto-generated init?(rawValue:)
  • Type conversion - String to Int, JSON to Model
  • Resource loading - Files, images that may not exist

Important Rules

  • All stored properties must be set before returning nil
  • Can override failable with non-failable (not vice versa)
  • Subclass failable init can delegate to superclass failable init

init! (Implicitly Unwrapped)

init! returns an implicitly unwrapped optional. Use sparingly - it will crash if nil is assigned to a non-optional!

main.swift
// FAILABLE INITIALIZERS
// Initializers that might fail and return nil

// BASIC FAILABLE INITIALIZER
struct Temperature {
    var celsius: Double

    init?(celsius: Double) {
        // Absolute zero is -273.15°C
        if celsius < -273.15 {
            return nil
        }
        self.celsius = celsius
    }
}

let valid = Temperature(celsius: 25)     // Optional(Temperature)
let invalid = Temperature(celsius: -300) // nil

// Using with optional binding
if let temp = Temperature(celsius: 100) {
    print("Temperature: \(temp.celsius)°C")
}

// FAILABLE INIT WITH ENUM
enum Direction: String {
    case north, south, east, west

    // Automatically generated failable init for raw values
}

let dir = Direction(rawValue: "north")  // Optional(Direction.north)
let bad = Direction(rawValue: "up")     // nil

// CUSTOM FAILABLE ENUM INIT
enum HTTPStatus {
    case ok
    case notFound
    case serverError
    case unknown(Int)

    init?(code: Int) {
        switch code {
        case 200: self = .ok
        case 404: self = .notFound
        case 500: self = .serverError
        case 100...599: self = .unknown(code)
        default: return nil  // Invalid HTTP code
        }
    }
}

let status = HTTPStatus(code: 200)  // .ok
let invalid2 = HTTPStatus(code: 999) // nil

// FAILABLE VS THROWING INITIALIZERS
// Failable (init?) - Returns nil on failure
// Throwing (init throws) - Throws specific error

// Failable - simple yes/no
struct Age {
    var value: Int

    init?(value: Int) {
        guard value >= 0, value <= 150 else {
            return nil
        }
        self.value = value
    }
}

// Throwing - detailed error info
struct AgeThrows {
    var value: Int

    enum AgeError: Error {
        case negative
        case tooOld
    }

    init(value: Int) throws {
        if value < 0 {
            throw AgeError.negative
        }
        if value > 150 {
            throw AgeError.tooOld
        }
        self.value = value
    }
}

// CHAINING FAILABLE INITIALIZERS
class Animal {
    var name: String

    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

class Dog: Animal {
    var breed: String

    init?(name: String, breed: String) {
        if breed.isEmpty { return nil }
        self.breed = breed
        super.init(name: name)  // Can fail!
    }
}

let dog = Dog(name: "Max", breed: "Labrador")  // Optional(Dog)
let badDog = Dog(name: "", breed: "Poodle")    // nil

// OVERRIDING FAILABLE INIT
class Document {
    var name: String?

    init() { }

    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

class AutoNamedDocument: Document {
    override init() {
        super.init()
        self.name = "Untitled"
    }

    // Override failable with non-failable
    override init(name: String) {
        super.init()
        if name.isEmpty {
            self.name = "Untitled"
        } else {
            self.name = name
        }
    }
}

// IMPLICITLY UNWRAPPED FAILABLE (init!)
struct Config {
    var apiKey: String

    init!(apiKey: String) {
        if apiKey.isEmpty { return nil }
        self.apiKey = apiKey
    }
}

// Returns implicitly unwrapped optional
let config: Config = Config(apiKey: "abc123")  // Force unwrapped
// let badConfig: Config = Config(apiKey: "")  // Crash!

// PRACTICAL EXAMPLE: URL Validation
struct ValidatedURL {
    let url: URL

    init?(string: String) {
        guard let url = URL(string: string),
              url.scheme == "https" else {
            return nil
        }
        self.url = url
    }
}

let validURL = ValidatedURL(string: "https://apple.com")
let invalidURL = ValidatedURL(string: "not-a-url")  // nil

// COMBINING WITH RESULT
enum ValidationResult<T> {
    case success(T)
    case failure(String)
}

struct Email {
    let address: String

    init?(address: String) {
        guard address.contains("@"),
              address.contains(".") else {
            return nil
        }
        self.address = address
    }
}

Try It Yourself!

Create a CreditCard struct with a failable initializer that validates the card number using the Luhn algorithm!