An Unexpected Edge Case with Swift's return

February 1, 2017

I recently came across a return corner case when debugging some Swift code, and I wanted to to share the story to save someone else the frustration I endured. Some of the blame should definitely be placed on me and my editor settings, however I think the Swift compiler can also improve how it handles non-standard return calls.

The code in question was a view controller I was working on in tandem with a co-worker, and we divided responsibilities between working on the layout and implementing business logic. He had already implemented some of the business rules for sorting and filtering data from the network, but it wasn't quite done yet. Picture it similar to this skeleton code:

    func refreshData() {
        var results = loadResultsFromCache()
        
        applyFilteringRules(results)
        applySortingRules(results)
        displayResults(results)
	}

I then start working on the layout and want to hardcode a wide variety of test data, so I decide to skip all of my coworker's data manipulation code. Typically you'll either comment out the code with +/ to Comment Selection or wrap the lines in /* and */, but sometimes it can be easier to write your temporary test code and use return to skip the remainder of the method. I chose to do the latter and ended up with this code:

	func refreshData() {
        //var results = loadResultsFromCache()
		var results = [SomeModelObject]()

		let testObject = SomeModelObject()
		// some configuration goes here
		results.append(testObject)

		let testObject2 = SomeModelObject()
		// some configuration goes here
		results.append(testObject2)

		...

		displayResults(results)

		return 


        applyFilteringRules(results)
        applySortingRules(results)
        displayResults(results)
	}

I happily go about writing some layout code, but when I run the app some of my test objects don't appear. I start looking for ambiguous constraints that could result in size of CGSize.zero or maybe multiple views are stacked on top of each other, but can't find any obvious errors. When the view debugger doesn't even show the correct number of views, I start to look at results and determine it doesn't contain all the elements it should. At my wits end, I put a breakpoint in applyFilteringRules just to see if it was somehow being run from another part of the code, and sure enough the debugger hits it and unexpectedly shows the call site is in refreshData!

The Solution

In most languages (including Objective-C), the return statement would end with a semicolon to terminate the line, but in Swift semicolons aren't needed. When you combine this with Swift's trailing closure syntax and the fact that closures return Void by default, you end up in the scenario where the test code actually executes return applyFilteringRules(results). This is understandably non-obvious, so the compiler helpfully warns you of this behavior (see the relevant test case):

Expression following 'return' is treated as an argument of the 'return'
Indent the expression to silence this warning
Unfortunately for me, this warning wasn't shown or I would have been able to short-circuit my debugging journey immediately. My first thought was that we had uncovered a compiler bug, but the real reason was a little more mundane: my coworker uses 4 spaces for indentation, but I use tabs. Xcode defaults to a tab width of 4 spaces, so the code looked visually identical but the compiler saw the difference in whitespace and interpreted it as an intentional change to silence the warning. Ideally once clang-format supports Swift, this kind of error would be transparently fixed by Xcode or as part of a pre-commit hook.

My Proposal

It seems too easy to hit this corner case and inadvertently silence the warning, so I propose forcing the user to explicitly declare a block to have it execute in this scenario. You would end up with writing (admittedly ugly!) code to make it very clear you want this behavior:

	func refreshData() {
		//var results = loadResultsFromCache()
		var results = [SomeModelObject]()

		let testObject = SomeModelObject()
		// some configuration goes here
		results.append(testObject)

		let testObject2 = SomeModelObject()
		// some configuration goes here
		results.append(testObject2)

		...

		displayResults(results)

		return {
			applyFilteringRules(results)
		}()

		applySortingRules(results)
		displayResults(results)
	}
Alternatively you could require the compiler to see the same type of whitespace on both lines to silence the warning, but I'm sure you could concoct some other scenario where inadvertent execution could still happen.