Get line causing error in catch?

Hi
So if I have something that goes like this

import Somelib
do {
    try someFunction()
    ...
catch {
    print(error)
}

Asuming I have more than call to one function with an error, Is there a way I can also get the line causing the error?
Note, Not using XCode here.

You mean the line that did the actual throw error, or the whole "stack of lines" like so:

foo.swift:123
bar.swift:456
someFunction.swift:789
someLib.swift:111 // the line where `try someFunction()` is

To do that automatically swift would have to modify the "error" being returned (e.g. add a userValue dictionary key with, file/line info). Swift doesn't do this.

For you own app you can throw NSError and include the relevant file/line info as a userInfo key.

func makeError(... file: String = __FILE__, line: Int = __LINE__) -> Error {
    let userInfo = ["errorFile": file, "errorLine": line]
    let error = NSError(..., userInfo: userInfo)
    return error
}

...
if ... {
    throw makeError(...) // this file + line would be in the error
}

Is this for debugging during development or at runtime (e.g. to print to some log)?

For the former, you should be able to break on swift_willThrow in lldb.

For the latter, you use a wrapper error type and a helper function to wrap errors thrown from the functions you call:

struct SourceLocationError<T: Error>: Error {
    let file: String, line: Int, error: T
}

func annotateError<T>(_ proc: @autoclosure () throws -> T, file: String = #file, line: Int = #line) throws -> T {
    do {
        return try proc()
    } catch {
        throw SourceLocationError(file: file, line: line, error: error)
    }
}

And then use it like this:

do {
    try annotateError(someFunction())
} catch {
    print(error)
}

Sadly, from what I tried, pre-/postfix operators cant have extra arguments, even when they have default values. That would have made it a bit less visually obtrusive, but if that is an issue a shorter wrapper function name should go a long way.

1 Like

Hi
@2, A call stack would be nice, Although I don't mind just the file and line number
@3, Yes it's for loggingز
All the ways shown here seems cool just except that I have to write an extra function for everything that errors, So I wonder if there's something easier?
I just want whenever I have an error raised I can log it in a way so I know where it happened, I rarely handle errors within the inner functions and just make it go to the highest function where main is because it works for my use case.

Nice!

Extending on your example (with recording "a stack" of thrown locations plus function names):

import Foundation

struct FileLocation: CustomStringConvertible {
    let file: String
    let line: Int
    let function: String

    var description: String {
        "\(file):\(line) \(function)"
    }
}

struct SourceLocationError: Error, CustomStringConvertible {
    var fileLocations: [FileLocation]
    let error: Error
    
    var description: String {
        let locs = fileLocations.reduce("") { e, loc in
            e.appending("\(loc)\n")
        }
        return "\(error)\n\(locs)"
    }
}

func annotateError<T>(_ proc: @autoclosure () throws -> T, file: String = #fileID, line: Int = #line, function: String = #function) throws -> T {
    do {
        return try proc()
    } catch {
        if let error = error as? SourceLocationError {
            var error = error
            error.fileLocations.append(FileLocation(file: file, line: line, function: function))
            throw error
        } else {
            throw annotatedError(error, file: file, line: line, function: function)
        }
    }
}

func annotatedError(_ error: Error, file: String = #fileID, line: Int = #line, function: String = #function) -> Error {
    SourceLocationError(fileLocations: [FileLocation(file: file, line: line, function: function)], error: error)
}

enum SomeError: Error {
    case someError
    case someOtherError
}

func foo(with: String, and: Int) throws {
    throw annotatedError(SomeError.someError)
    // throw SomeError.someError
}

func bar() throws {
    try annotateError(foo(with: "", and: 1))
}

func baz(with: String) throws {
    try annotateError(bar())
}

func test() {
    do {
        try annotateError(baz(with: "Hello"))
    }
    catch {
        let err = error
        print(err)
    }
}

test()

Gives this output:

someError
JT/main.swift:51 foo(with:and:)
JT/main.swift:56 bar()
JT/main.swift:60 baz(with:)
JT/main.swift:65 test()

without using annotatedError when throwing the error "call stack" misses the entry for "foo" but is still reasonable:

someError
JT/main.swift:56 bar()
JT/main.swift:60 baz(with:)
JT/main.swift:65 test()

The file locations are spot on👌:

1 Like