Error Handling in Swift Does Not Involve Stack Unwinding. What Does It Mean

Error handling in Swift does not involve stack unwinding. What does it mean?

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.

What is stack unwinding?

Stack unwinding is usually talked about in connection with exception handling. Here's an example:

void func( int x )
{
char* pleak = new char[1024]; // might be lost => memory leak
std::string s( "hello world" ); // will be properly destructed

if ( x ) throw std::runtime_error( "boom" );

delete [] pleak; // will only get here if x == 0. if x!=0, throw exception
}

int main()
{
try
{
func( 10 );
}
catch ( const std::exception& e )
{
return 1;
}

return 0;
}

Here memory allocated for pleak will be lost if an exception is thrown, while memory allocated to s will be properly released by std::string destructor in any case. The objects allocated on the stack are "unwound" when the scope is exited (here the scope is of the function func.) This is done by the compiler inserting calls to destructors of automatic (stack) variables.

Now this is a very powerful concept leading to the technique called RAII, that is Resource Acquisition Is Initialization, that helps us manage resources like memory, database connections, open file descriptors, etc. in C++.

Now that allows us to provide exception safety guarantees.

Is it correct to call an error created with Swift's keyword throw an exception?

I would argue that why it is called error handling has nothing to do with the call stack. This is corroborated by the fact that based on the definitions of both words, the call stack is irrelevant. I believe that the purpose of the quote that you have included in your question is merely to provide a distinction between error-handling in Swift and exception-handling in other languages, disregarding the differences in names.

To identify the distinction in terms of non-swift languages, a common description is that it is an error not to handle an exception. Thus an error and an exception are two unique entities.

Now in Swift, it seems that they have tried to completely get rid of the word "exception" in favor of just using the term error. Thus an error can be dealt with, and if not, the program crashes. This is likely because it is not as important what the actual crash is called, and what is more important is what caused it, in this case an "error".

In terms of usage in the iOS world, I have little experience in this, but I would assume that even though to call it "exception handling" is not technically correct, most Swift programmers would know what you are talking about and probably not correct you (or even think to correct you).

Overall, I think it is mostly a matter of semantics, and not what constitutes an "error" and what constitutes an "exception".

Edit

I should clarify that I mean that the difference between errors and exceptions within iOS/Swift is not just semantics. Exceptions are what are thrown when illegal things happen, and errors can be created to allow you to prevent these messages from being shown/your program crashing. In this respect they are completely different things.

My point is that among different coding languages the term "error" (in Swift) and "exception" (in Java for example) are basically the same, just with different names.

For example, I could try and deal with an error named "ArrayError" (stupid name I know, it's just for an example) where as in Java I could be trying to catch IndexOutOfBoundsException. Both of these objects are both thrown and caught, and thus I am drawing a comparison between the two highlighting the differences in naming conventions in Swift vs other languages.

But no, errors and exceptions are not technically the same thing, even in Swift.

How to pass an Error up the stack trace in Swift

Referring to Swift - Error Handling Documentation, you should:

1- Create your custom error type, by declaring enum which conforms to Error Protocol:

enum CustomError: Error {
case error01
}

2- Declaring foo() as throwable function:

func foo() throws {
throw CustomError.error01
}

3- Declaring bar() as throwable function:

func bar() throws {
try foo()
}

Note that although bar() is throwable (throws), it does not contain throw, why? because it calls foo() (which is also a function that throws an error) with a try means that the throwing will -implicitly- goes to foo().

To make it more clear:

4- Implement test() function (Do-Catch):

func test() {
do {
try bar()
} catch {
print("\(error) has been caught!")
}
}

5- Calling test() function:

test() // error01 has been caught!

As you can see, bar() automatically throws error, which is referring to foo() function error throwing.

Why the swift throws exception despite try?

try? does not catch exceptions. It catches thrown errors. Those are different things in Swift. Exceptions are at the Objective-C level and cannot be caught by Swift at all (they can't be safely caught in ObjC in most cases either, but that's a different discussion).

The solution in this case is to use JSONEncoder rather than JSONSerialization. JSONEncoder is a pure-Swift system. JSONSerialization is bridged from ObjC.

let body = try? JSONEncoder().encode([data])

See Handling Errors for more information:

Error handling in Swift resembles exception handling in other languages, with the use of the try, catch and throw keywords. Unlike exception handling in many languages—including Objective-C—error handling in Swift does not involve unwinding the call stack, a process that can be computationally expensive. As such, the performance characteristics of a throw statement are comparable to those of a return statement.

If you want to use JSONSerialization, it's important to recognize that it is a programming error to call it this way. The exception is intended to crash the program (even in ObjC). The correct way to write this code is:

if JSONSerialization.isValidJSONObject([data]), // <=== first, check it is valid
let body = try? JSONSerialization.data(withJSONObject: [data]) {
print("success")
} else {
print("unable to make body for call")
}

See the docs for more information:

If obj will not produce valid JSON, an exception is thrown. This exception is thrown prior to parsing and represents a programming error, not an internal error. You should check whether the input will produce valid JSON before calling this method by using isValidJSONObject(_:).

The thrown error from JSONSerialization is only to indicate an internal error in the serializer, not an attempt to encode an invalid object:

error

If an internal error occurs, upon return contains an NSError object with code NSPropertyListWriteInvalidError that describes the problem.

Swift 2.1 do-try-catch not catching error

The do - catch combination is fine. This issue is simply one that cannot be caught - and therefore never makes it to the catch block.

If the issue were catchable (defined and handled via Swift's throws functionality), the catch block would've been executed.


Some semantics: there is a long-standing argument about the differences between the terms error and exception.

Some developers consider the two terms to be distinct. In this case, the term error represents an issue that was designed to be handled. Swift's throws action would fit here. In this case, a do - catch combination would allow the issue to be caught.

An exception, for these developers, represents an unexpected, usually fatal, issue that cannot be caught and handled. (Generally, even if you could catch it, you would not be able to handle it.)

Others consider the two terms to be equivalent and interchangeable, regardless of whether the issue in question can be caught or not. (Apple's documentation seems to follow this philosophy.)

(Updated to focus on the answer rather than the semantics.)

Does throwing seriously affect performance in Swift?

Think of throw and catch in Swift as merely a safe, controlled form of goto or jump. It's safe and controlled because the compiler sees to it that you cannot throw unless there is someone there to catch. What will happen under what circumstances is thus completely known at compile time.

That is totally different from Objc exceptions where nothing is known in advance, and at runtime we just start walking blindly up the call stack introspecting at every step, and ending up with a possible crash.

Can I use std::current_exception during stack unwinding?

C++ Standard defines current_exception() in section § 18.8.5 [propagation] :

(emphasis mine)

exception_ptr current_exception() noexcept;

Returns: An exception_ptr object that refers to the currently handled
exception (15.3) or a copy of the currently handled exception, or a
null exception_ptr object if no exception is being handled. The
referenced object shall remain valid at least as long as there is an
exception_ptr object that refers to it.

And § 15.3 [except.handle], notes 7 and 8 :


  1. A handler is considered active when initialization is complete for the
    parameter (if any) of the catch clause. [ Note: The stack will have
    been unwound at that point
    . — end note ]

  2. The exception with the most recently activated handler that is still
    active
    is called the currently handled exception.

The exception returned by current_exception() is defined as the "currently handled exception", which is the exception of the most recent active handler, and a handler is active only when stack unwinding completed.


As your tests have shown, there is no "active handler" during stack unwinding, so there is no "currently handled exception" either : in that case, current_exception() will return a null exception_ptr.



Related Topics



Leave a reply



Submit