Forced Unwrapping URLs

Let me start with a little background. Laptopmini wanted to know why this code wasn’t compiling. Admittedly, Swift’s error messages leave room for improvement. The tl;dr was: he needed to swap the 2nd and 3rd lines of code. This allows both properties to be initialized before the code refers to self.


I immediately fixated on those exclamation points. “Are there any circumstances that these URLs will fail during creation? And if so, don’t you want to control that crash?” Laptop pointed out that there was no circumstance where his URLs would be malformed, and it was perfectly safe to use the exclamation point.

Despite this, his approach bugged my aesthetics. There’s no reason those URLs should be created in that initializer. I figured there were better ways to deal with this, moving URL creation out of the initializer and controlling crash landings with better error messages.

First, I considered extending URL to offer a safer non-optional initializer. The following snippet offers “controlled descent”, providing valuable feedback for any string that cannot be used to construct a URL.

extension URL {
    /// Non-optional initializer with better fail output
    public init(safeString string: String) {
        guard let instance = URL(string: string) else {
            fatalError("Unconstructable URL: \(string)")
        }
        self = instance
    }
}

This seemed too big a hammer for this problem. Instead, I decided to recommend the same approach with a solution localized to the SocketEndPoint type:

// Returns `URL` with guided fail
public enum SocketEndPoint: String {
    case events = "http://nowhere.com/events"
    case live = "http://nowhere.com/live"
    
    public var url: URL {
        guard let url = URL(string: self.rawValue) else {
            fatalError("Unconstructable URL: \(self.rawValue)")
        }
        return url
    }
}

This approach allows his init code to simplify. Under the updated design, it uses ordinary URLs in the initializer and no exclamation points.

// His initializer
fileprivate init() {
    self.eventSocket = WebSocket(url: SocketEndPoint.events.url))
    self.liveSocket = WebSocket(url: SocketEndPoint.live.url))
    self.eventSocket.delegate = self
    self.liveSocket.delegate = self
}

Long story short:

  • String enumerations are handy but SocketEndPoint wasn’t pulling its weight. Its job is to deliver named URLs. It should do that no matter however the type is implemented.  Don’t let types slack off because there’s an “almost there” solution baked into the language.
  • Keep initializers clean and simple.
  • When weighing a universal solution against a simple local one, sometimes it’s best to think small and fix the problem in the immediate context.
  • I considered normal URL construction (returning an Optional) and creating a WebSocket initializer with implicitly unwrapped optionals (public init(url: URL!)This approach offered no real advantages and lost the ability to report which string was malformed. Also, ugly.

You can see a few of approaches I kicked around at this gist.

12 Comments

  • There is a typo in public var url: URL?: the type should be a non-optional URL (as in the gists)

    • Fixed, thanks!

  • In public enum SocketEndPoint, it says:

    public var url: URL? { ... }

    I think it should be

    public var url: URL { ... }

    Right? Otherwise, a very good solution, I like it a lot.

    • Thanks, fixed!

  • Is “tl;dr” such well known slang (I had to look it up) that it should be used in the somewhat formal atmosphere of a technical blog post. Also, “Too Long/Didn’t Read” doesn’t seem to match up with her usage in context, which is more along the lines of “long story short” or “the upshot was”.

    • It’s pretty ubiquitous in the tech community. I can refer you to the site editor to file a formal complaint if you’d like to follow up more diligently.

    • “TL;DR” is just a modern more-snarky synonym for “summary” or “recap” or “in conclusion”.

  • I don’t like !, but in this case I don’t see much difference from fatalError, given both will crash anyway.

    What is the advantages of fatalError instead something like this:


    public var url: URL {
    return URL(string: self.rawValue)!
    }

    • Fatal error lets you explain what went wrong rather than a simple crash. It’s a documented exit strategy.

  • Hi Erica
    The SocketEndPoint values used in the initializer (SocketEndPoint.live.url and SocketEndPoint.events.url) don’t match with the declared cases (liveSocket and eventSocket respectively)

    Great post otherwise!
    Fred

    • Thanks, fixed!

  • Noticed a typo:

    I believe you meant WebSocket(url: SocketEndPoint.events.url) instead of WebSocket(url: SocketEndPoint.events.url)) (note the extra )), and WebSocket(url: SocketEndPoint.live.url) instead of WebSocket(url: SocketEndPoint.live.url)).

Join the Discussion

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>