Failable and throwing initializers


Greetings, traveler!

Sometimes, an object’s initializer takes a particular type, but we must limit the possibility of passing some variants of values of this type to avoid initialization. Let’s look at a practical example.

We have a vehicle factory that can produce sedans and coupes.

By the way, a factory can also be a design pattern in your project. You can read about the Factory Method and the Abstract Factory as part of a series of articles on design patterns.

enum VehicleKind {
    case coupe
    case sedan
    case truck
}

struct Vehicle {
    let kind: VehicleKind
    
    init(kind: VehicleKind) {
        self.kind = kind
    }
}

But we have difficulties with trucks. We cannot make them because we do not have the necessary components in production.

We can create a check before initializing the object to limit the possibility of producing tracks.


class VehicleFactory {
    func makeVehicle(vehicleKind: VehicleKind) {
        guard vehicleKind != .truck else {
            return
        }
        
        let vehicle = Vehicle(kind: vehicleKind)
    }
}

However, we can make the code cleaner and eliminate the need to perform such a check before each attempt to initialize the vehicle. To do this, we must process the incoming value inside the initializer. Of course, we can do something like this.

struct Vehicle {
    let kind: VehicleKind
    
    init(kind: VehicleKind) {
        switch kind {
        case .coupe, .sedan:
            self.kind = kind
        case .truck:
            fatalError("Can't produce this type of vehicle")
        }
    }
}

However, this is an unsafe and non-obvious option, which can lead to application crashes.

Failable initializer

To avoid such problems, we can create a failable initializer.

struct Vehicle {
    let kind: VehicleKind
    
    init?(kind: VehicleKind) {
        switch kind {
        case .coupe, .sedan:
            self.kind = kind
        case .truck:
            return nil
        }
    }
}

Now, when initializing the object, we can get a nil value.

class VehicleFactory {
    func makeVehicle() {
        let vehicle = Vehicle(kind: .truck) // nil
        let vehicle2 = Vehicle(kind: .coupe) // Vehicle?
    }
}

But in this case, to fully understand why we didn’t get the value, we will have to set breakpoints or log various cases to conduct an investigation.

Throwing initializer

To automatically collect information, we can make a throwing initializer.

struct Vehicle {
    
    enum VehicleError: Error {
        case badVehicle
    }
    
    let kind: VehicleKind
    
    init(kind: VehicleKind) throws {
        switch kind {
        case .coupe, .sedan:
            self.kind = kind
        case .truck:
            throw VehicleError.badVehicle
        }
    }
    
}

Now, we can fully understand why we failed to initialize the object.

class VehicleFactory {
    func makeVehicle() {
        do {
            let vehicle = try Vehicle(kind: .truck)
        } catch {
            print(error) // VehicleError.badVehicle
        }
    }
}

However, we will have to use a rather cumbersome do-catch construction. This allows us to receive error reports, but we can write simpler code if we don’t need it.

class VehicleFactory {
    func makeVehicle() {
        let vehicle = try? Vehicle(kind: .truck) // nil
    }
}

Conclusion

That’s all I wanted to say about this topic. I hope you enjoyed this article. See you soon.