Printing enum cases (why does Xcode print "SwiftMODBUS.MBError error 5" but my code prints "Timeout"?)

In my MODBUS library, I have a slew of errors in an enum (MBError).
In a unit test in Xcode, when the test throws an exception, Xcode writes this to the console:

<unknown>:0: error: -[SwiftMODBUSTests.SwiftMODBUSTests testAlicatAsync] : failed: caught error: "The operation couldn’t be completed. (SwiftMODBUS.MBError error 5.)"

If I catch the error and print it, I get the CustomDebugStringConvertible I created for it:

Err: Timeout

I figured the "5" was the fifth case in the enum, but that doesn't match. Here's the enum:

public
enum
MBError : Error
{
	case unknown(Int)
	case deviceIDNotSet
	case unexpectedReturnedRegisterCount(Int)
	case timeout
	
	//	libmodbus errors
	
	case invalidFunction
	case invalidAddress(Int?)
	case invalidValue(Int?, String?)
	case serverFailure
	case ack
	case serverBusy
	case nack
	case memoryParity
	case notDefined			//	???
	case gatewayPathUnavailable
	case noResponse
	case invalidCRC
	case invalidData
	case invalidExeceptionCode
	case unknownExeceptionCode
	case dataOverflow		//	Too many bytes returned
	case badServer			//	Response not from requested device
	…
}

extension
MBError : CustomDebugStringConvertible
{
	public
	var
	debugDescription: String
	{
		switch self
		{
			case .unknown(let ec):								return "Unknown (\(ec))"
			case .deviceIDNotSet:								return "Device ID not set"
			case .unexpectedReturnedRegisterCount(let c):		return "Unexpected returned register count: \(c)"
			case .timeout:										return "Timeout"
			
			case .invalidFunction:								return "Invalid function"
			case .invalidAddress(let a):						return "Invalid address \(String(describing: a))"
			case .invalidValue(let a, let v):					return "Invalid value \(String(describing: a)) \(String(describing: v))"
			case .serverFailure:								return "Server failure"
			case .ack:											return "Acknowledged"
			case .serverBusy:									return "Server busy"
			case .nack:											return "Not acknowledged"
			case .memoryParity:									return "Memory parity"
			case .notDefined:									return "Error not defined"
			case .gatewayPathUnavailable:						return "Gateway path unavailable"
			case .noResponse:									return "No response"
			case .invalidCRC:									return "Invalid CRC"
			case .invalidData:									return "Invalid data"
			case .invalidExeceptionCode:						return "Invalid exception code"
			case .unknownExeceptionCode:						return "Unknown exception code"
			case .dataOverflow:									return "Data overflow"
			case .badServer:									return "Bad server"
		}
	}
}

I believe that's what happens when a Swift Error is printed from Obj-C (which underlies XCTest), as it converts to NSError, which requires a numeric code. I've never found the logic for how the numeric values are generated.

1 Like

Ah, it's all in swift/ErrorType.swift at 23ba70e7bac709874ae06a4545d287fb27333995 · apple/swift · GitHub

Looks like it's all generated metadata from the runtime. In some brief experiments the _code value generally follow the pattern of cases with associated values first, then cases without associated values.

This:

enum SomeError: Error, CaseIterable {
    case one, two, three(String), four, five, six(String), seven
    
    static var allCases: [SomeError] {
        [.one, .two, .three("default"), .four, .five, .six("default"), .seven]
    }
}

print(SomeError.allCases.map(\._code))

prints [2, 3, 0, 4, 5, 1, 6].

3 Likes

Ah! That explains why the numbering changed when I added those associated values. Nice sleuthing.