Hard to find which function throws error

public enum MyError : Error {
    case somethingWrong
}

public func f1(x: Int) throws -> Int {
    if x > 10 {
        return x
    } else {
        throw MyError.somethingWrong
    }
}

public func f2(x: Int) throws -> Int {
    if x < 20 {
        return try f1(x: x)
    } else {
        throw MyError.somethingWrong
    }
}

public func f3() {
    do {
        let value = 7
        try f2(x: value)
    } catch {
        print(error) //I can get the error "somethingWrong", but I don't know it's failed by which function, f1() or f2()
    }
}

f3()

Hi, All:
The code above described my issue, that when I catch some error, I don't know which function throws it, to address that I have to add print function under each try call, Usually, the calling hierarchy is very deep. Is there any way easy to solve this issue?

Would case somethingWrong(StaticString) suit your case? Then you can do: throw MyError.somethingWrong(#function)

My use case is that I have a custom video composition class, it has a deep calling hierarchy, some error is thrown by AVFoundation, some thrown by custom error, it's not possible to change the AVError parameter, and to add a parameter to the custom error parameter is still not that good!

This is in the nature of handling errors by throwing and catching, ie it is designed to only propagate the error, it does not propagate the information (nested structure) about where in the call hierarchy the error happened.

So I guess your only option is to wrap any existing error handling within something that will keep track of where the error happened. You could use a Result enum or you could change your MyError type like @DevAndArtist suggested, possibly with this modification:

enum MyError : Error {
    case someError(thrownBy: StaticString)
    case someIndirectError(Error, catchedAndRethrownBy: StaticString)
    init(_ throwingFnName: @autoclosure () -> StaticString = #function) {
        self = .someError(thrownBy: throwingFnName())
    }
    init(indirectError ie: Error,
         _ catchingFnName: @autoclosure () -> StaticString = #function)
    {
        self = .someIndirectError(ie, catchedAndRethrownBy: catchingFnName())
    }
}

struct SomeAVFoundationError : Error { }

func someAVFoundationFn(_ v: Int) throws -> Int {
    // Values that are multiples of 5 are reported as an error by this func:
    guard v % 5 != 0 else { throw SomeAVFoundationError() }
    return v
}

func f1(x: Int) throws -> Int {
    // Values less than or equal to 10 are reported as an error by f1:
    guard x > 10 else { throw MyError() }
    // Catch and rethrow errors that we have no control over by wrapping them
    // in an inderect error:
    do {
        return try someAVFoundationFn(x)
    } catch {
        throw MyError(indirectError: error)
    }
}

func f2(x: Int) throws -> Int {
    // Values greater than or equal to 20 are reported as an error by f2:
    guard x < 20 else { throw MyError() }
    return try f1(x: x)
}

func f3(x: Int) throws -> Int {
    // Even values are reported as an error by f3:
    guard x % 2 != 0 else { throw MyError() }
    return try f2(x: x)
}

do { print(try f3(x: 17)) } catch { print(error) }
// 17

do { print(try f3(x: 7)) } catch { print(error) }
// someError(thrownBy: "f1(x:)")

do { print(try f3(x: 27)) } catch { print(error) }
// someError(thrownBy: "f2(x:)")

do { print(try f3(x: 8)) } catch { print(error) }
// someError(thrownBy: "f3(x:)")

do { print(try f3(x: 15)) } catch { print(error) }
// someIndirectError(SomeAVFoundationError(), catchedAndRethrownBy: "f1(x:)")

Thanks for your solution. In this case, the purpose is to find the line of code that crashed, so I can quickly fix the problem. To refactor the code like this can help, but the code will look messy. If I can add a breakpoint at the line of catch {}, then I can see the error throw stack, that will be the best solution!

Last time I had this problem, I was thinking about adding a global configuration flag crashOnError, only active in debug builds, which turns try into try! when set.

Bug -> Set the flag -> Reproduce (crash) -> Inspect stack trace -> Fix the bug -> Remove the flag.

If you run your app in a debugger, it should pause whenever it throws an error.

Well, you'll have to enable the "Swift Error Breakpoint" in Xcode's breakpoint view first.

3 Likes

Yeah, it works, thank you very much!