Catchable is a Swift macro that can be applied to any protocol to automatically generate a decorator that enables the injection of a custom error processor, simplifying the handling and transformation of errors thrown by conforming types.
Consider the following example with a protocol and its conforming class:
protocol FooProtocol {
func foo() throws
func bar() throws
}
final class FooService {
enum Error: Swift.Error {
case critical
}
func foo(path: String) throws {
throw Error.critical
}
}
final class Foo: FooProtocol {
private let service: FooService
init(service: FooService) {
self.service = service
}
func foo() throws -> [String] {
try service.foo(path: "foo")
}
func bar() throws {
try service.foo(path: "foo")
}
}The Foo class can throw a variety of errors originating from multiple dependencies. Since not all of these errors are user-friendly or localized, you might want to map them to a custom application-specific error type to improve user experience:
enum AppError: LocalizedError {
case somethingWentWrong
var errorDescription: String? {
switch self {
case .somethingWentWrong: "Oops!"
}
}
}
final class Foo: FooProtocol {
private let service: FooService
init(service: FooService) {
self.service = service
}
func foo() throws {
do {
try service.foo(path: "foo")
} catch {
throw AppError.somethingWentWrong
}
}
func bar() throws {
do {
try service.foo(path: "bar")
} catch {
throw AppError.somethingWentWrong
}
}
}In this version, all errors are mapped to AppError, making them more suitable for display to the user. However, this manual approach introduces repetitive boilerplate code, especially when applied across multiple methods or classes.
Catchable eliminates this boilerplate by generating a decorator that wraps your implementation and applies a reusable error processor, streamlining error handling with minimal effort.
Here is how the same example looks using the @Catchable macro:
@Catchable
protocol FooProtocol {
func foo() throws
func bar() throws
}
// `ErrorProcessor` is a protocol provided by the `Catchable` library.
// It allows custom logic for handling or transforming errors.
struct AppErrorProcessor: ErrorProcessor {
func callAsFunction(_ error: Error) -> Error {
switch error {
case FooService.Error.criticalError: AppError.somethingWentWrong
default: error
}
}
}
final class Foo: FooProtocol {
private let service: FooService
init(service: FooService) {
self.service = service
}
func foo() throws {
try service.foo(path: "foo")
}
func bar() throws {
try service.foo(path: "bar")
}
}
// `catchable(errorProcessor:)` is automatically generated by the macro.
let userFriendlyFoo = Foo(service: FooService()).catchable(errorProcessor: AppErrorProcessor())In this setup, userFriendlyFoo wraps all method calls in error processing logic. Any error thrown by userFriendlyFoo methods is passed through AppErrorProcessor, which transforms the FooService.Error.critical error into AppError.somethingWentWrong.
You can use the Catchable macro not only to map errors but also to process them in any way you need. For example, to log errors to a remote service:
struct AppErrorProcessor: ErrorProcessor {
func callAsFunction(_ error: Error) -> Error {
RemoteService.log(error)
return error
}
}This makes error-handling logic highly reusable, whether it's for mapping, logging, analytics, or custom error reporting.
The Catchable macro works by generating a private decorator class named CatchableDecorator, which conforms to the annotated protocol. Due to current macro system constraints, only one @Catchable macro can be used per file. To apply Catchable to multiple protocols, place each annotated protocol in a separate source file.
Catchable is released under the MIT license. See the LICENSE file for more info.