Skip to content

Support error chain description with opening indent level #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

jubishop
Copy link
Contributor

@jubishop jubishop commented May 3, 2025

Proposed Changes

Sometimes I still want to wrap an error manually, and provide additional messaging on both the wrapper level and the leaf level. the problem with simple catch/caught is only the bottom leaf level prints any details. here's a simplified example use case:

indirect enum SearchError: KittedError {
  case fetchFailure(request: URLRequest, networkError: NetworkError)
  case caught(Error)

  var userFriendlyMessage: String {
    switch self {
    case .fetchFailure(let request, let networkError):
      return
        """
        Failed to fetch url: \(request)
          \(ErrorKit.errorChainDescription(for: networkError, indent: "  "))
        """
    case .caught(let error):
      return ErrorKit.userFriendlyMessage(for: error)
    }
  }
}

by being able to pass my indent I can get this formatted nicely, an example end result printout when I print an errorChainDescription of a .fetchFailure is:

SearchError.fetchFailure(request: https://api.podcastindex.org/api/1.0/podcasts/trending?lang=en, networkError: ErrorKit.NetworkError.caught(Error Domain=NSURLErrorDomain Code=-1011 "(null)"))
└─ userFriendlyMessage: "Failed to fetch url: https://api.podcastindex.org/api/1.0/podcasts/trending?lang=en
  NetworkError
  └─ NSError [Class]
     └─ userFriendlyMessage: "A network error occurred: The operation couldn’t be completed. (NSURLErrorDomain error -1011.)""

id be more than happy to entertain alternative on how to achieve something like this. this approach still only allows theoretical details to come out the top and the bottom of an error chain, everything in between would still be erased, but this is at least an improvement, and exposing the indent variable with a default of "" doesn't seem like its harming much.

a larger change would be to provide a way to print a more verbose error chain that does something like checking at every depth level to see if the Error is Throwable including the userFriendlyMessage when it exists, or to also include the String(describing at every level.

@jubishop
Copy link
Contributor Author

jubishop commented May 4, 2025

I'm going to formulate some more on how to solve this but I still think this change should stand. Users may want to start their error chain printout at an indented level in logs.

@jubishop
Copy link
Contributor Author

jubishop commented May 4, 2025

hey so I kind of came up with my own solution to errorChainDescription, which I will paste below. this means this change is no longer important to me, although I would still lean towards giving users the flexibility to print their error chain with any initial indent depth. here's the code I've come up with and some tests that hopefully indicate how it's used and works. it just uses userFriendlyMessage all the way down..

protocol KittedError: Throwable, Catching, Equatable {}

extension KittedError {
  static func == (_ lhs: Self, _ rhs: Self) -> Bool {
    lhs.userFriendlyMessage == rhs.userFriendlyMessage
  }

  static func typeName(for error: Error) -> String {
    String(describing: type(of: error))
  }

  static func typeAndCaseName(for error: Error) -> String {
    let mirror = Mirror(reflecting: error)
    let typeName = String(describing: type(of: error))

    guard let caseName = mirror.children.first?.label else { return typeName }
    return "\(typeName).\(caseName)"
  }

  static func nested(_ message: String) -> String {
    message
      .components(separatedBy: .newlines)
      .joined(separator: "\n  ")
  }

  static func nestedUserFriendlyMessage(for error: Error) -> String {
    nested(ErrorKit.userFriendlyMessage(for: error))
  }

  func nestedUserFriendlyCaughtMessage(_ caught: Error) -> String {
    Self.typeName(for: self) + " ->\n  " + Self.nestedUserFriendlyMessage(for: caught)
  }

  func nestedUserFriendlyMessage() -> String {
    Self.nested(userFriendlyMessage)
  }
}
enum FakeFormattedError: KittedError {
  case doubleFailure(one: any KittedError, two: any KittedError)
  case failure(underlying: any KittedError)
  case leaf
  case leafUnderlying(underlying: Error)
  case caught(Error)

  var userFriendlyMessage: String {
    switch self {
    case .doubleFailure(let one, let two):
      return
        """
        Failure
          One: \(one.nestedUserFriendlyMessage())
          Two: \(two.nestedUserFriendlyMessage())
        Done
        """
    case .failure(let underlying):
      return
        """
        Failure
          \(underlying.nestedUserFriendlyMessage())
        """
    case .leaf:
      return
        """
        Leaf
        Line Two
          Indented Line
        """
    case .leafUnderlying(let underlying):
      return
        """
        Leaf
        Wrapping:
          \(Self.nestedUserFriendlyMessage(for: underlying))
        """
    case .caught(let error):
      return nestedUserFriendlyCaughtMessage(error)
    }
  }
}

@Suite("of KittedError formatting tests")
struct KittedErrorFormattingTests {
  private let repo: Repo = .inMemory()

  @Test("messages with caught generic at end")
  func testMessagesCaughtGenericAtEnd() {
    let error = FakeFormattedError.failure(
      underlying: FakeFormattedError.failure(
        underlying: FakeFormattedError.failure(
          underlying: FakeFormattedError.caught(
            GenericError(userFriendlyMessage: "Generic edge case")
          )
        )
      )
    )

    #expect(
      error.userFriendlyMessage == """
        Failure
          Failure
            Failure
              FakeFormattedError ->
                Generic edge case
        """
    )
  }

  @Test("messages with caught kitted at end")
  func testMessagesCaughtKittedAtEnd() {
    let error = FakeFormattedError.failure(
      underlying: FakeFormattedError.failure(
        underlying: FakeFormattedError.failure(
          underlying: FakeFormattedError.caught(
            FakeFormattedError.leaf
          )
        )
      )
    )

    #expect(
      error.userFriendlyMessage == """
        Failure
          Failure
            Failure
              FakeFormattedError ->
                Leaf
                Line Two
                  Indented Line
        """
    )
  }

  @Test("messages with caught kitted in middle")
  func testFormattingNestedUserFriendlyMessagesKittedAtEnd() {
    let error = FakeFormattedError.failure(
      underlying: FakeFormattedError.failure(
        underlying: FakeFormattedError.failure(
          underlying: FakeFormattedError.caught(
            FakeFormattedError.failure(
              underlying: FakeFormattedError.failure(
                underlying: FakeFormattedError.leafUnderlying(
                  underlying: GenericError(userFriendlyMessage: "Generic edge case")
                )
              )
            )
          )
        )
      )
    )

    #expect(
      error.userFriendlyMessage == """
        Failure
          Failure
            Failure
              FakeFormattedError ->
                Failure
                  Failure
                    Leaf
                    Wrapping:
                      Generic edge case
        """
    )
  }

  @Test("messages with double failure")
  func testFormattingDoubleFailure() {
    let error = FakeFormattedError.failure(
      underlying: FakeFormattedError.failure(
        underlying: FakeFormattedError.doubleFailure(
          one: FakeFormattedError.caught(
            FakeFormattedError.failure(
              underlying: FakeFormattedError.failure(
                underlying: FakeFormattedError.leafUnderlying(
                  underlying: GenericError(
                    userFriendlyMessage:
                      """
                      Generic edge case
                      Heyo
                        Indented
                      """
                  )
                )
              )
            )
          ),
          two: FakeFormattedError.failure(
            underlying: FakeFormattedError.failure(
              underlying: FakeFormattedError.leaf
            )
          )
        )
      )
    )

    #expect(
      error.userFriendlyMessage == """
        Failure
          Failure
            Failure
              One: FakeFormattedError ->
                Failure
                  Failure
                    Leaf
                    Wrapping:
                      Generic edge case
                      Heyo
                        Indented
              Two: Failure
                Failure
                  Leaf
                  Line Two
                    Indented Line
            Done
        """
    )
  }
}

@jubishop
Copy link
Contributor Author

jubishop commented May 5, 2025

I've also gone ahead and marked the catch await closure as @sendable

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant