6

According to the docs,

error handling in Swift does not involve unwinding the call stack, a process that can be computationally expensive

I wonder what does it mean? I always thought of stack unwinding as a process of calling destructors properly in case of exception (in terms of C++).

So I decided to model the situation:

class A {
    init() {
        print("Inited")
    }
    deinit {
        print("Deinited")
    }
}

func f() throws {
    let a = A()
    throw MyError.e
}

The output was:

Inited
Deinited

So the "destructor" was called - and it means (in my understanding) that stack unwinding works in Swift.

Can anybody explain why docs says that it is

not involved

?

Nick
  • 3,205
  • 9
  • 57
  • 108

2 Answers2

11

Stack unwinding is just the process of navigating up the stack looking for the handler. Wikipedia summarizes it as follows:

Some languages call for unwinding the stack as this search progresses. That is, if function f, containing a handler H for exception E, calls function g, which in turn calls function h, and an exception E occurs in h, then functions h and g may be terminated, and H in f will handle E.

Whereas a Swift error doesn't unwind the stack looking for a handler. It just returns, and expects the caller to handle the thrown error. In fact, the sentence after the one you quote goes on to say:

As such, the performance characteristics of a throw statement are comparable to those of a return statement.

So, using that first example, where f called g which calls h, in Swift, if you want f to catch the error thrown by h, then:

  • h must explicitly be marked that it throws errors;
  • g must explicitly try its call to h;
  • g must also be marked that it throws errors, too; and
  • f must explicitly try its call to g.

In short, while some other languages offer stack unwinding in the process of finding the exception handler, in Swift error handling, you must either explicitly catch the error thrown by functions you try, or be designated as a function that throws so that failed try calls will be thrown back up to the caller. There is no automatic unwinding of the stack in Swift.

All of this is unrelated to the question of whether deallocation takes place. As you've seen, yes, the throw in Swift behaves much like return, deallocating those local variables.

It's worth noting that not all exception handling that involves stack unwinding does the deallocation. Generally it does (because of course we want it to clean up when we're handling exceptions), but for example, "the GNU C++ unwinder does not call object destructors when an unhandled exception occurs. The reason for this is to improve debuggability." (From Exception Handling in LLVM.) Clearly, that's only appropriate for unhandled exceptions in debugging environments, but it illustrates the issue that unwinding the stack doesn't necessarily mean that objects are deallocated.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 1
    Let's say an exception handler is 10 stack frames above the point of the exception. Why is returning 10 times (as in Swift) any faster than unwinding the 10 stack frames (as in C++)? Aren't we doing the same work (find the address of the caller's frame, jump to it; repeat 10 times)? – max Dec 25 '19 at 08:34
  • The technical details of how exception handling is done varies from language to language (and in ObjC, even between 32-bit and 64-bit executables). But in general, Objective-C is optimized for the non-exception scenario and the exception handler apparently introduces additional overhead. But that is all somewhat academic, as this simply is not how Swift works. It an error handling system, not an exception handling system, and the error handling behavior of functions simply must be declared throughout every step of the call tree where errors can be thrown. It simply is how Swift works. – Rob Aug 03 '21 at 04:27
0

It's true that if swift stack unwinds then it will call all the destructor of all the objects that were allocated since the beginning of the block. But the converse might not be true. Just because the destructor was called for your object A does not imply that swift stack unwinds. Also if you really wanted to test if it is stack unwinding you should try a more rigorous example

TNguyen
  • 1,041
  • 9
  • 24