String-like errors in Swift without monkey patching

~ 3 mins

A while back I read about a neat trick.

extension String: Swift.Error {}

I am a bit afraid of it.

By adding this line, you can now throw "Any string" since Strings now conform to Error. However, I’ve always been a bit afraid of monkey patching things that are not mine. It tends to cause problems. This is personal FUD.

There are strings other than just String!

Swift has many neat protocols, and we’ll be using three in particular today to make a new kind of error that we’ll call ErrorMessage.

Error

A type representing an error value that can be thrown

By adopting this protocol on our types, we make it possible to throw them. If you use Reactive Swift, for example, the error type of all signals must have adopted this protocol.

By adopting this protocol, we can throw any ErrorMessage.

LosslessStringConvertible

A type that can be represented as a string in a lossless, unambiguous way.

By adopting this protocol, we’re saying that every ErrorMessage is a String; keep in mind that we’re not saying that every String is an ErrorMessage.

ExpressibleByStringLiteral

A type that can be initialized with a string literal.

Remember that we said before that not every String is an ErrorMessage? By adopting this protocol, we now said that it is. Every String is also an ErrorMessage.

Without this protocol we would need to explicitly wrap our error messages as ErrorMessage(…) every frickin’ time. With this protocol, however:

let error: ErrorMessage = "Oh no, something went wrong!"

Yes, that’ll work. No, I’m not pulling your leg. Yes, this is the bees knees.

All of us together, now!

struct ErrorMessage: Swift.Error, ExpressibleByStringLiteral, LosslessStringConvertible {
	let description: String
	
	// ExpressibleByStringLiteral
	init(stringLiteral string: String) {
		self.description = string
	}
	
	// LosslessStringConvertible
	init(_ description: String) {
		self.description = description
	}
}

Usage

If you have no type information available you’ll need to cast it.

func crashBangBoom() throws {
	throw "Boom!" as ErrorMessage
}

let error: ErrorMessage = "Oh no!"

However, if you do have inferred type information and can play with string literals then things will be smooooth.

Note: This example uses ReactiveSwift.

let producer = SignalProducer<String, ErrorMessage> { observer, lifetime in
	observer.send(error: "Oh no!")
}

Tips

You might want to check out LocalizedError, it has some more of that error handling goodness.

That was all. Be well!

rss facebook twitter github youtube mail spotify instagram linkedin google pinterest medium rubygems