Moving toward deprecating force unwrap from Swift?

In an ideal world programs would crash quickly during development but never in production. I've never found a way to bring that about but it's personal belief that a language feature that inclines programs toward failing if the developer is not vigilant is not ideal. For me, on balance, force unwrap falls into this category. I have the feeling I've never written a force unwrap that hasn't tripped me up later and had to be recoded. Perhaps that's overstating it. It certainly is the case when my code crashes it's almost always a "force something" which I should never have been given the license to use in the first place.

1 Like

If safety was our only concern, we should also make the Array subscript optional and EnvironmentObjects as well. Semantically, that would be more correct and therefore it would be safer. However, the first one would come at a tremendous speed cost and the second one is really quite safe the way it is because you can be quite sure it will crash in DEBUG so that faulty code just won't make it into production; having bulky optionals there would make your code just a lot harder to read and most likely the user-experience from some default view that isn't supposed to ever show up (and is therefore probably not well-designed) will not be much better than a crash.

My rule of thumb is:

  1. Avoid force-unwrap whenever nil is a legit possibility.
  2. If you use optional values to model a form that gets filled out, create an additional "filled out form" type without optionals and only show a "submit" button if you can actually construct the "filled out form" value (use a guard let in code).
  3. If you write library code where not dealing with optionals is waaay more readable for the library user even though a value could be nil, hide the optional behind a computed property and do a guard let with a fatalerror and a nice error message in the else case. Also, document your code properly so the using programmer understands how to use it correctly.
  4. In non-library code, cautiously use force-unwrap whenever you are 99.9% certain that the value exist on that code-path (e.g. because it's in a private method and you can prove the value has been set in all public methods calling the private method) and all of the above techniques are just too bulky.

Depending on what exactly you are writing, you can end up with very different mixes of the above methods.

6 Likes

One possibly related thing I have occasionally wondered if it would be reasonable to add is
an @_total/@_total(warn) (name TBD) function attribute. This would error/warn if the function could not be proven to eventually either return or throw.

This would be relevant because in an @_total context, a force unwrap would be either invalid or its operand is inlined and never nil.

In general I kind of agree with the reasons behind the depreciation, but I think there are still times where the postfix can be necessary... the earlier example of
let url = URL(ā€œhttps://forums.swift.orgā€)!

Make sense, I am putting in a literal, one would hope that unit tests would find when I have the literal incorrect.

On the other hand if I was doing something along the lines of

let url = URL(someStringFromAnOutsideSource)!

Would be opening myself up to errors, I cannot always be sure of the value from that outside source and thus should be much more careful with it.

I donā€™t think itā€™s a perfect solution but surely if you are returning an explicitly unwrapped optional in a public function, you are opening the caller to unknown behaviour in some cases and that is when you should be encouraged to either return an optional, a Result<> or be throwing...

Sometimes in private usage the programmer is asserting outright they are expecting something to not be nil and if it isnā€™t, then you should have a fatal error... but in public facing APIā€™s it makes more sense to highlight you are opening problems up to the caller...

2 Likes

One way I've accomplished this on my own types is by having two initializers: one for a String that can fail, and one that takes a StaticString that asserts. I figure if you're willing to type in a literal string, you should get the benefit of the doubt.

For example:

public struct Regex {
    private let inner: NSRegularExpression

    public init(_ pattern: StaticString) {
        self.inner = try! NSRegularExpression(pattern: pattern.description, options: [])
    }

    public init(_ pattern: String) throws {
        self.inner = try NSRegularExpression(pattern: pattern, options: [])
    }
}

In this example, a regular expression that I init with a literal as assumed to be correct. But when creating a regular expression with an arbitrary string, it's allowed to fail. It would be great if URL did something similar.

18 Likes

This is an example of what I was talking about above.

Iā€˜m neutral on the pitch. Personally I would welcome the change, but I donā€˜t mind if it stays. However I would appreciate if we had a direct function on an Optional type to unwrap it in a single chain. Sure there is a property for that already, but IIRC it does not have the same checks as the operator function does.

Furthermore here are my two cents on the IBOutlet and similar attributes. Those should be replaced with more modern and safe property wrappers instead. These should be part of UIKit and not the language itself. IBOutlet is just like a deferred im/mutable property wrapper from the proposal. In fact, if it was a PW like deferred im/mutable it would eliminate the need of IUO or a regular optional and improve the codebase ergonomics.

3 Likes

I think it would be useful if a linter/compiler tool existed that could highlight/warn on all uses of force-unwrap ! in source code. For instance, if you are writing a library/SDK framework that is intended to be used by others, you likely want to ensure that all crashes come with a descriptive console error message rather than the generic unwrapping message you get with the ! operator.

Deprecating ! altogether is obviously a non-starter. ! is not inherently bad, even though some Swift programming courses mislead students by teaching with that mindset. It has genuine uses when the programmer knows more than the static compiler does, or when you don't need fine-grained error handling (like in scripting contexts or fast prototyping stages).

Of course, we can ā€” and should ā€” reduce the places where a ! is redundantly required (like with the URL(_: StaticString) example).

5 Likes

I was initially against this proposal, but I'm coming around. It's too late to deprecate !, but I would definitely +1 a pitch that nudged new code towards a clearer, neat alternative that gave better diagnostics. In addition to other things people have mentioned, there's also the point that when code gets reviewed as part of a PR process or something similar, the ! operator can all too easily disappear into other changes, when it is often something that should trigger a bit more scrutiny from the reviewer.

Afair there has been a pitch for

returnOptional() !! "I really did not expect nil here. Hope this message helps"

It's just not easy to find it :-)
As others already said, force unwrapping is here to stay, and there are situations where it is the right tool.

4 Likes

I've prepared a minute compiler patch that adds new -force-unrwap-warning and -force-unwrap-error flags which would be all I'd like to see at this stage. I'll file a PR when I've written a test and the gentle folk on the compiler team have had a chance to clear their inboxes from the holidays.

1 Like

This would be inconsistent with Swift's goal not to have language "dialects" with differing rules. The compiler deliberately has no toggles to turn off or on any warnings or errors.

As others have said, you can use linters for this purpose. However, Swift has a pretty clear opinionated approach on safety and unwrapping that disagrees with your approach, and also an opinionated approach to not permitting dialects.


[Edit] If you were able to equip the compiler to identify specific scenarios where force unwrapping is certain to be an error, then by all means those should be made an error. But that's to be distinguished from usages which are not to one's taste, or usages that are certain to successfully unwrap (which are in fact desirable usages of !).

13 Likes

Compiler flags like the ones suggested here, and warnings in general, have faced resistance in the past because the core team has wanted to avoid creating language dialects in Swift.

Later in that thread (which was about warnings in general, not force-unwrapping), @Joe_Groff calls out something that I think is especially relevant to this discussion:

In the case of force-unwrap, I don't think a fix-it/suggestion could cover all possible cases well and be useful to the userā€”it would be far too broad. And in many cases, if the user of the force-unwrap can guarantee that it won't trap due to other preconditions already known to hold, it may make more sense to just use ! and a suggestion to use if/guard let would harm the legibility of the code.

FWIW, I used to be fairly strongly on the side of "force-unwrap is a code smell", and when misused, it definitely is. But my experience has changed my mind to reinforce its legitimate uses as a language feature and that neither a compiler warning nor even a lint warning is something I'm comfortable with enforcing in my own code. (swift-format has rules to warn if force-unwrap/force-try are used, and we've turned them off by default because they were too noisy.) So I think if someone does want to enforce something stronger, enabling a linter in their own codebases is fine, but it's not something that the compiler should support or encourage.

17 Likes

If thereā€™s anything I regret with the ! force unwrap operator, it is that it collides with the ! negation operator. That makes it harder to spot in a cursive reading of code. Kotlin uses a double bang (!!) for force unwraps, which is more Ā»noisyĀ«, and something which I would have preferred.

As a contractor, Iā€™ve encountered my fair share of Ā»just make it workĀ« force unwraps in code written by programmers schooled in different languages like Python or Objective-C, and have spent much time eliminating those uses for code that can be read without the ever-present fear that there is a crash hiding in the middle of a function or clause.

Nevertheless, I have come to consider force unwraps as simple shorthand for guard ā€¦ else { fatalError("This should be unreachable") but do try to mark those cases with the presumption at play. One can refactor most unwraps as the guard above, but that is nevertheless mostly noise, since the only straight thing to do in such situations is to crash ā€” the problem with force unwraps is when theyā€™re used to shut the compiler up, when they should simply be used to signal that a crash is desirable if a precondition is not met.

2 Likes

Consider naming the function forceUnwrap.

1 Like

As an alternative, what about catching at the errored position and elevating an error message to the user asking them to try again or contact support? (Talk to your copy editor for the best wording)

Iā€™d assume in any case, if the app crashed an annoyed user would either be confused and do nothing (or delete the app), or perform one of those two suggested options.

1 Like

Both swiftlint and swift-format do exactly this.

3 Likes

I've pushed a new version of the Unwrap package to do just this and removed the -force-unwrap-error option from the prospective compiler patch to address any criticism that it might somehow reduce the functionality of the compiler.

But given constraints such as:

this pitch just about reaches "check mate" and may not be able to be progressed in any direction. I'm sympathetic to the notion that code should never require options in order to compile which was probably the downfall of this proposal but I'd argue adding an opt-in warning doesn't create a new dialect in that code compiled with the warning enabled always compiles in the default configuration.

The idea of "strict" compilation options is not novel but whether this is more the role of a linter is certainly up for debate. Why single-out force unwraps when there are other sources of exceptions such indexing off the end of an array or bad casting? I'd argue it is the prevalence of it's (mis-)usage that tips the balance.

The underlying problem is Swift has not engineered the concept of a catchable long throw (when a program encounters an exceptional condition) into its implementation and with good reason ā€” it is impossible to implement without compromise. There is a means of sorts to graft it into the language at runtime but that particular piece of code is likely the worst piece of Swift ever written (It's intended for use in Web servers where crashing out is simply not good form.) Some other languages embrace the compromise, for example, (incredibly) when an uncaught exception percolates up to the runloop of a C# application a generic "an exception has occurred" dialogue can be presented automatically to the user offering them the opportunity to continue, an offer I've found useful to take up on occasion.

1 Like

Honestly, I havenā€™t seen any successful case when a code became more stable with force unwrap warnings (or errors). IMHO, crashing while calling ! doesnā€™t look better than unsafeUnwrap(...), and people, who have problems with deciding where force unwrapping is suitable, still misuse this pattern even if they replace ! to guard let ... else { fatalError("Shout exist") }.
I would like to have something like constexpr from C++, to avoid force unwrapping for cases like let max = [x, y, z].max()! or URL(string: "https://apple.com")!, but it seems a big feature requiring a serious investigation.

3 Likes

I donā€™t really see what the problem with force unwraps is. If you don't like them then you can avoid them through other means. For everyone else they are an incredibly useful tool for working around a fundamental problem: that type systems can't account for everything (or rather, they can, but you inevitably end up with worse code in many cases if you try).

Can force unwrapping be misused? Sure. So can generics or the new async/await or switch statements. Not everything needs to be enforced at a language level, and a bit of ā€œcommon senseā€ and ā€œbest practiceā€ is ok to apply.

For cases mentioned above, where you've already done a nil check, or where you create a URL or access a dictionary key and a nil result is a programmer error, then force unwrap lets you enforce that while maintaining clarity for anyone reading (which is the primary function of any code). As such, I'm not sure any sort of deprecation or replacement is necessary (or even desirable) for the language at large

18 Likes