Error handling: with both propagation and specific handling

My use case: I am performing 3 or more operations in a row. Each operation can throw an error and I want to properly propagate these errors up the chain.

enum OperationError: Error {
    case anything
}
enum LargeOperationError: Error { 
    case parseFailureField(field: String, message: String)
}
func performLargeOperation() -> Result<T, LargeOperationError> {
    let result1 = operation1()
    // if result1 is .failure, return a new .failure(.parseFailureField("op 1", result1's error description))
    // else, continue in the same scope with the .success variable
    
    let result2 = operation2()
    // if result2 is .failure, return a new .failure(.parseFailureField("op 2", result2's error description))
    // else, continue in the same scope with the .success variable
    
    let result3 = operation3()
    // if result3 is .failure, return a new .failure(.parseFailureField("op 3", result1's error description))
    // else, continue in the same scope with the .success variable
    
    // do something with the success results of the above three operations
}

My real-world use case for this is deserializing fixed width integers off of a network byte stream and constructing a packet object with the information.

Approach 1: Do/get/catch
Here, we can use .get() throws of the Result<Success, Failure> and catch everything and throw a new error from there.

do {
    let value1 = try operation1().get()
    let value2 = try operation2().get()
    let value3 = try operation3().get()
    return .success(T(value1, value2, value3))
} catch is OperationError {
    return .failure(.parseFailureField("???", error.description))
}

The problem here is that we don't know which operation of the three failed. The failure we can return can propagate the information of the originally returned failure (error.description), but the calling code would not know which part failed in here.

Approach 2: guard
Here, we can use guard statements to capture the information of which operation failed.

let result1 = operation1()
guard case let .success(value1) = result1 else {
    return .failure(.parseFailureField("operation 1", "???"))
}
let result2 = operation2()
guard case let .success(value2) = result2 else {
    return .failure(.parseFailureField("operation 2", "???"))
}
let result3 = operation3()
guard case let .success(value3) = result3 else {
    return .failure(.parseFailureField("operation 3", "???"))
}
return return .success(T(value1, value2, value3))

We've sacrificed our error propagation for our ability to provide detailed, specific error reporting. We can report which of the parts within our larger operation has failed which aids in debugging, but we don't have information about what caused the error anymore.

Approach 3: nested switch
This is the safest way to properly handle everything as needed.

let result1 = operation1()
switch result1 {
case let .failure(err):
    return .failure(.parseFailureField("operation 1", err.description))
case let .success(value1):

    let result2 = operation2()
    switch result2 {
    case let .failure(err):
        return .failure(.parseFailureField("operation 2", err.description))
    case let .success(value2):

        let result3 = operation3()
        switch result3 {
        case let .failure(err):
            return .failure(.parseFailureField("operation 3", err.description))
        case let .success(value3):
            return return .success(T(value1, value2, value3))
        }
    }
}

This follows the "return early" pattern [0], but it's nested and isn't linear. We can improve this somewhat like so.

let result1 = operation1()
let value1: T
switch result1 {
case let .failure(err):
    return .failure(.parseFailureField("operation 1", err.description))
case let .success(_value1):
    value1 = _value1
}

let result2 = operation2()
let value2: T
switch result2 {
case let .failure(err):
    return .failure(.parseFailureField("operation 2", err.description))
case let .success(_value2):
    value2 = _value2
}
let result3 = operation3()
let value3: T
switch result3 {
case let .failure(err):
    return .failure(.parseFailureField("operation 3", err.description))
case let .success(_value3):
    value3 = _value3
}
return return .success(T(value1, value2, value3))

We now have both propagation of the original error, as well as more specific error handling. But wow, look at all of the code we need to add for something that should be a lot simpler. This is unfortunately the best solution I've found so far.

Approach 4: the forbidden ! operator
We can vastly simplify the code, at the expense of safety.

let result1 = operation1()
guard case let .success(value1) = result1 else {
    return .failure(.parseFailureField("operation 1", result1.error!.description))
}
let result2 = operation2()
guard case let .success(value2) = result2 else {
    return .failure(.parseFailureField("operation 2", result2.error!.description))
}
let result3 = operation3()
guard case let .success(value3) = result3 else {
    return .failure(.parseFailureField("operation 3", result3.error!.description))
}
return return .success(T(value1, value2, value3))

extension Result {
    var error: Failure? {
        switch self {
        case let .failure(err):
            return err
        case .success(_):
            return nil
        }
    }
}

But if someone messes up the code somehow, we suddenly get unexpected force unwraps of the nil .error property in the extension, which is something to avoid.

Discussion
It's at this point that I'm not sure how best to move forward. My goals would be to minimize the amount of boilerplate code, to handle both propagation and field-specific handling as outlined above, and to avoid unsafe practices such as using the force unwrap operator.

Is this where using Result's .map, .flatMap, .mapError, and/or .flatMapError functions would come into play, possibly?

Some guidance on how to handle errors with these constraints would be appreciated!

Possible proposal
Pondering the above, I had the idea of a change to the guard statement to make this all easier. Essentially, while the guard case let binds a variable to the scope outside of the block, if it's binding to a Result<Success, Failure> (or possibly any enum with two cases), there could be a new bound variable on the inside of the guard statement of the other case. This would essentially be syntactical sugar to Approach 3. If this post does not yield sufficient results, I may make a separate post outlint this in more detail under the Evolution category of the forum.

[0]: The "return early" pattern

One thing I can think of, is to have a temporary variable for the current query:

do {
  var current = "op 1"
  _ = operator1()
} catch {
  //use Current here
}

It may also be worthwhile to have operator1 itself throws LargeOperationError. IMO, performLargeOperation knows too much about how errors should be handle. Rather, if you can’t make do with OperationError, it probably contains too little information to be of use.

Personally, I don’t think wrapping errors is much more beneficial in Swift error system anyway.

It’s not really a problem if you’re using stdlib Result. The values are local, and so guarantees success to those unwraps anyway. This is a very safe usage of force unwrap.

You can also do something like this:

// Note: 'operationKeyMap' is a tuple array instead of a dictionary to ensure order.
let operationKeyMap: [(String, () -> Result<T, OperationError>)] = [("op1", operation1), ("op2", operation2), ("op3", operation3)]
let finalResult: Result<[T], LargeOperationError> = Result {
    try operationKeyMap.map {
        switch $0.1() {
            case .success(let value): return value
            // This will cause an early termination of the map loop.
            case .failure(let error): throw LargeOperationError.parseFailureField($0.0, error.description)
        }
    }
// This is needed because the throws returns an 'Error'
}.flatMapError { .failure($0 as! LargeOperationError) }

switch finalResult {
  case .success(let values): // Do something with [T]
  case .failure(error): // Do something with LargeOperationError.parseFailureField
}

Given that you just want to capture the “operation number”, it might be worth encoding it into the OperationError enum instead. If an operation function (like operation1) fails, it can just return .failure(OperationError.whatever(step: 0)) and you can extract it from the error (perhaps via a convenience stepValue property on the enum).

How about this?


struct FailedResult: Error {
    var mesage: String
    var cause: Error

    init(_ message: String, _ cause: Error) {
        self.mesage = message
        self.cause = cause
    }
}

do {
    let a = try operation1().mapError { FailedResult("op1", $0) }.get()
    let b = try operation2().mapError { FailedResult("op2", $0) }.get()
    let c = try operation3().mapError { FailedResult("op3", $0) }.get()
    print(a, b, c)
} catch let error as FailedResult {
    print(error.mesage, error.cause)
} catch {
    fatalError("unexpect type: \(error)")
}

Hi all, has there been any further work on this? I'm running into exactly the same problem, and I can't see any satisfactory solution in the current language docs.

James posted a great explanation of the problem at the start of this thread; here's my summary:

  • I want to ensure that both successes and failures are handled (so I need either Result or throws)
  • I want compile-time checks that all failure cases are covered and handled correctly (so I need Result)

These steer me towards Result, but there's no easy way to unpack results except switch, which leads to excessive nesting and/or boilerplate.

Both of the proposed solutions lose compile-time safety as they convert failures into untyped exceptions, which have to be downcast again. It's easy to write this correctly once, less easy to ensure that it stays correct as the code is modified over time. Exceptions are great when errors are unrecoverable, and you just need to blow away the current subsystem and restart from scratch; but sometimes you really do want specific error handling.

What I'd love is a guard statement that will extract the failure case into the body of the guard, and the success case into the normal control flow.

An alternative would be an extension to throws that lets you specify the thrown type; but I'd understand if there's reluctance here as it would be more or less equivalent to Java's checked exceptions (which I personally like, but I'm in a minority there!)

Is anything along those lines in the works? Or is there another solution I've overlooked?

Thanks!

Iain

Hello Iain, there sure is something in the pipes, and you'll get a detailed outline in this post: Precise error typing in Swift.

EDIT: I have been a little quick in my answer. A guard ... catch is sometimes discussed, but nothing has turned into a pitch yet.

1 Like

Thanks for the quick reply!

Just to check I understand, that proposal is to add precise error types to throws; and then with the addition of a hypothetical guard ... catch, you can unpack and handle errors without extra nesting?

That seems like quite a lot of new stuff to wait for, not all of which may actually land...

I'm personally happy with Result, so I think just a guard-like syntax for unpacking successes and failures into two control flow branches without nesting would be great. I wonder if that's a useful line of inquiry, as it wouldn't need any semantic changes at all? Unless exceptions are generally preferred over Result, as John's proposal seems to imply.

Hi @iainmerrick,

Apologies for not following up on my own post! I looked back at the code that I was writing around the time of this post, and this is what I ended up writing. Hopefully it suits your needs in some way. I think it's terse enough to work well.

public func deserialize(from buffer: inout ByteBuffer) -> Result<Packet, PacketDeserializationHandlerError> {
	// Id
	guard let id = buffer.readInteger(endianness: .big, as: UInt32.self) else {
		return .failure(.needMoreData)
	}

	// Link Path
	let linkPathResult = buffer.readSftpString()
	guard case let .success(linkPath) = linkPathResult else {
		return .failure(linkPathResult.error!.customMapError(wrapper: "Failed to deserialize link path"))
	}

	// Target Path
	let targetPathResult = buffer.readSftpString()
	guard case let .success(targetPath) = targetPathResult else {
		return .failure(targetPathResult.error!.customMapError(wrapper: "Failed to deserialize target path"))
	}

	return .success(.createSymbolicLink(CreateSymbolicLinkPacket(id: id, linkPath: linkPath, targetPath: targetPath)))
}

The linkPathResult etc. are Result<T,U> types. The guard case let .success(linkPath) = linkPathResult else {} prevents excessive nesting. The only downside is getting the error via linkPathResult.error!. Human error may occur if you're not careful, but copying/pasting and getting the wrong result variable. I think it's an okay tradeoff, in the end.

The .customMapError(wrapper:) is a custom extension on the specific Error type all of the results share.

The Precise Error post looks interesting!

1 Like

Hi James, thanks for following up. Yeah, that pattern looks pretty good and is working well for me. I had independently kind of reconstructed it myself, and didn't realise until afterwards that it was actually the final suggestion in your original post — doh! I must not have read it quite right the first time.

I wanted to avoid extra nesting because I was writing asynchronous code with completion callbacks, so there's already unavoidable nesting there. But luckily that guards pretty well against the possible bug you describe, where the wrong error value gets force-unwrapped, as each one is generally in a different scope.

However, I'm now thinking of updating the code to use async / await. That could finally be really readable, but then that potential bug becomes a bit more likely! Oh well… :person_shrugging:

I really think this is one of the biggest remaining warts in the language, at least that I've come across so far. guard is really nice when it works, but there are a few situations where it doesn't. Having done a fair bit of Kotlin as well, I often find myself really missing Kotlin's "smart casts" in Swift, where the compiler can narrow types implicitly without any special syntax support. In this case, if I have a Result and I've already established that it's not a Success, it would be awesome if the compiler were able to determine that it must be a Failure and therefore let me skip the ! when accessing error.