admin管理员组

文章数量:1290978

I'm working on a SwiftUI application where we use Sentry to catch any errors and send them to a server where we can analyze them later on. We are currently using Sentry but we could swith to any other tool if it offers better error handling, howerver, I believe the limitations we are facing are from the languge itself, and not the tool.

What I would like to know is how to handle errors in Swift in a way that I can always achieve this two objectives:

  1. get a meaningful stack trace of the error
  2. be able to show a custom error message to the user when the error occurs (this means that the app cannot crash ans close itself).

I've created a test application that showcases the different ways in which an error can be handled:

struct TestSentryApp: App {
    
    init() {
        
        //Initialize Sentry
        SentrySDK.start { options in
            options.dsn = "SECRET_DSN_GOES_HERE"
            options.debug = false
            options.tracesSampleRate = 1.0
            options.profilesSampleRate = 1.0
        }
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

//Define a custom error
enum CustomError : Error {
    case validationError(message: String)
    case internalError(message: String, innerError: Error? = nil)
}


struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("Error Handling Tests")
            
            Button("Test A") {
                try! throwingFunction()
            }
            
            Button("Test B") {
                do {
                    try throwingFunction()
                }
                catch {
                    //We could show a message to the user here informing about the error here
                    SentrySDK.capture(error: error)
                }
            }
            
            Button("Test C") {
                do {

                    try wrappedErrorThrowingFunction()
                }
                catch {
                    //We could show the message of the internalError here to the user, but still understand what happened because we have the innerError
                    SentrySDK.capture(error: error)
                }
            }
        }
    }
    
    func throwingFunction() throws {
        throw CustomError.validationError(message: "Error thrown on purpose.")
    }
    
    func wrappedErrorThrowingFunction() throws {
        do {
            try throwingFunction()
        }
        catch {
            throw CustomError.internalError(message: "Unexpecetd error", innerError: error)
        }
    }
}

Pressing each of the buttons behaves in the following way:

  • Button A: does not perform any special handling of the error and in return we get a clear stack trace of the error but the app crashes, so the user does not get any information about what went wrong.
  • Button B: captures any possible error, allowing us to inform the user but the stack trace of the error points to the line where we call SentrySDK.capture as the line where the error originated, which is not true.
  • Button C: shows another approach where we wrap any error with our own CustomError .internalError. This allows us to show the user the message from the internalError while still keeping (and logging) the innerError for debugging. This is imporant in situations where the innerError contains paths or other sensitive information we do not want to disclose to the user.

The question then is: -None of the above aproaches provides a full stack trace with the actual line of the error, but for the code in Button A, which crashes the app. Is there any way to always get a full stack trace without having to crash the app? -Can I wrap any error with my custom one while maitaining the original stack trace?

I'm working on a SwiftUI application where we use Sentry to catch any errors and send them to a server where we can analyze them later on. We are currently using Sentry but we could swith to any other tool if it offers better error handling, howerver, I believe the limitations we are facing are from the languge itself, and not the tool.

What I would like to know is how to handle errors in Swift in a way that I can always achieve this two objectives:

  1. get a meaningful stack trace of the error
  2. be able to show a custom error message to the user when the error occurs (this means that the app cannot crash ans close itself).

I've created a test application that showcases the different ways in which an error can be handled:

struct TestSentryApp: App {
    
    init() {
        
        //Initialize Sentry
        SentrySDK.start { options in
            options.dsn = "SECRET_DSN_GOES_HERE"
            options.debug = false
            options.tracesSampleRate = 1.0
            options.profilesSampleRate = 1.0
        }
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

//Define a custom error
enum CustomError : Error {
    case validationError(message: String)
    case internalError(message: String, innerError: Error? = nil)
}


struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("Error Handling Tests")
            
            Button("Test A") {
                try! throwingFunction()
            }
            
            Button("Test B") {
                do {
                    try throwingFunction()
                }
                catch {
                    //We could show a message to the user here informing about the error here
                    SentrySDK.capture(error: error)
                }
            }
            
            Button("Test C") {
                do {

                    try wrappedErrorThrowingFunction()
                }
                catch {
                    //We could show the message of the internalError here to the user, but still understand what happened because we have the innerError
                    SentrySDK.capture(error: error)
                }
            }
        }
    }
    
    func throwingFunction() throws {
        throw CustomError.validationError(message: "Error thrown on purpose.")
    }
    
    func wrappedErrorThrowingFunction() throws {
        do {
            try throwingFunction()
        }
        catch {
            throw CustomError.internalError(message: "Unexpecetd error", innerError: error)
        }
    }
}

Pressing each of the buttons behaves in the following way:

  • Button A: does not perform any special handling of the error and in return we get a clear stack trace of the error but the app crashes, so the user does not get any information about what went wrong.
  • Button B: captures any possible error, allowing us to inform the user but the stack trace of the error points to the line where we call SentrySDK.capture as the line where the error originated, which is not true.
  • Button C: shows another approach where we wrap any error with our own CustomError .internalError. This allows us to show the user the message from the internalError while still keeping (and logging) the innerError for debugging. This is imporant in situations where the innerError contains paths or other sensitive information we do not want to disclose to the user.

The question then is: -None of the above aproaches provides a full stack trace with the actual line of the error, but for the code in Button A, which crashes the app. Is there any way to always get a full stack trace without having to crash the app? -Can I wrap any error with my custom one while maitaining the original stack trace?

Share Improve this question edited Feb 13 at 18:02 HangarRash 15k5 gold badges19 silver badges55 bronze badges asked Feb 13 at 17:23 EnricEnric 3251 silver badge7 bronze badges 3
  • What stack trace are you referring to? This is Swift and not Java so there is no stack trace being created every time an error is thrown. – Joakim Danielson Commented Feb 13 at 17:37
  • You are right that the StackTrace terminology is not from Swift, I believe in Swift is called "callStackSymbols", but the meaning is the same. What I want is the list of call stack symbols that lead me to the actual line that caused the error. Is it possible? Or at least is there a better way of handling errors? – Enric Commented Feb 13 at 17:40
  • Stack trace or call stack symbols are something you only get when the app crashes. – Joakim Danielson Commented Feb 13 at 17:41
Add a comment  | 

1 Answer 1

Reset to default 1

If you are only throwing your own errors, then sure,

struct CustomError: Error {
    let message: String
    let innerError: (any Error)?
    let callStack: [String] = Thread.callStackSymbols
    
    init(message: String, innerError: (any Error)? = nil) {
        self.message = message
        self.innerError = innerError
    }
}

func throwingFunction() throws {
    throw CustomError(message: "Error thrown on purpose.")
}

func wrappedErrorThrowingFunction() throws {
    do {
        try throwingFunction()
    }
    catch {
        throw CustomError(message: "Unexpecetd error", innerError: error)
    }
}

This will record the call stack when the CustomError is created. You can print the call stack symbols like this:

Button("Test C") {
    do {
        try wrappedErrorThrowingFunction()
    }
    catch let error as CustomError {
        print(error.callStack.joined(separator: "\n"))
        if let cause = error.innerError as? CustomError {
            print("Caused by:")
            print(cause.callStack.joined(separator: "\n"))
        }
    }
    catch { }
}

For any arbitrary Error? That's not possible. The only requirement of conforming to Error is Sendable. Errors are not required to contain any call stack information.

try/catch/throw in Swift are just fancy control flow structures. As far as I can tell, not even the runtime keeps track of the call stack when an error is thrown.

本文标签: