Skip to content

Instantly share code, notes, and snippets.

@koher
Last active March 14, 2016 11:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save koher/e6a8b128bd7ad6898ac9 to your computer and use it in GitHub Desktop.
Save koher/e6a8b128bd7ad6898ac9 to your computer and use it in GitHub Desktop.
Proposal for error handling in Swift

throws as returning a Result

I think it would be great if throws -> Foo were a syntactic sugar of -> Result<Foo>. Without affecting existing codes, it makes it possible to go back and forth between Manual Propagation and Automatic Propagation seamlessly.

// I wish if the first `makeFoo` were
// a syntactic sugar of the second one
func makeFoo(x: Int) throws -> Foo {
  guard ... else {
    throw FooError()
  }
  return Foo(x)
}
// @warn_unused_result
// func makeFoo(x: Int) -> Result<Foo> {
//   guard ... else {
//     return Result(error: FooError())
//   }
//   return Result(Foo(x))
// }

// Manual propagation
let result: Result<Foo> = makeFoo(42) // without `try`
switch result {
  case let .Success(foo):
    ...
  case let .Failure(error):
    ...
}

// Automatic propagation
do {
  let foo: Foo = try makeFoo(42) // with `try`: a kind of unwrapping
  ...
} catch let error {
  ...
}

For what?

I want to unify throws and Result into one feature to keep the language simple.

As referred in "Error Handling Rationale and Proposal", I think Swift should provide something like Result. It means we would have similar two features: throws and Result. We need a way to covert them to each other. For examples, it can be done in the following way.

// What I DON'T want
func makeFoo(x: Int) throws -> Foo { ... } // -> Result<Foo>

let a: Result<Foo> = try| makeFoo(42)
  // `try|` for `Result` like `try?` for `Optional`

do {
  let b = try a.throwIfError()
  ...
} catch let error {
  ...
}

If throws were a syntactic sugar of returning a Result, it would be simpler.

// What I want
func makeFoo(x: Int) throws -> Foo { ... } // -> Result<Foo>

let a: Result<Foo> = makeFoo(42)

do {
  let b = try a
  ...
} catch let error {
  ...
}

In Addition, it prevents that APIs of third-party libraries diverge. If we had similar but different two features, throws and Result, some libraries would use throws and others would use Result. Actually it has already happened. Some popular libraries use antitypical/Result or their own Result types. If throws were a syntactic sugar of returning a Result, using throws or Result would affect only the appearance of codes, and we could use those libraries in the same way.

This is some supplementary explanations about throws as returning a Result.

  • What's Result?
  • Why we need both throws and Result
  • Result<Value> vs Result<Value, Error>
  • When forgets try
  • With side effects
  • Why not Either, union types nor tuples
  • Return value of do
  • Complication with rethrows

What's Result?

It's an enum declared in a following way.

enum Result<Value> {
  case Success(Value)
  case Failure(ErrorType)
}

It also should have map, flatMap and some convenient methods like Optional.

Why we need both throws and Result

Result provides more flexible way to handle errors than throws though they provide similar functionalities. It can be assigned to a variable, passed to a function and stored in a property while an error must be handled immediately after it is thrown. It is useful especially for asynchronous operations.

For example, think about map or flatMap (or then) method of Promise<Value> (or Future<Value>). They cannot receive a function with throws.

extension Promise {
  func map<T>(transform: Value -> T) -> Promise<T> { ... }
}

Because the transform is executed asynchronously, this map method cannot throw an error immediately. If we had throws as returning a Result, we can pass a function with throws to the map.

func toInt(x: String) throws -> Int { ... } // -> Result<Int>

let string: Promise<String> = ...
let number: Promise<Result<Int>> = string.map(toInt)

It also caused a problem when I implemented lazily evaluated List<Element>s.

extension List {
  func map<T>(transform: Element -> T) -> List<T> { ... }
}

It cannot throw an error because the transform is evaluated lazily. With throws as returning a Result, it could be used with a function with throws too.

func toInt(x: String) throws -> Int { ... } // -> Result<Int>

let strings: List<String> = ... // Infinite list
let numbers: List<Result<Int>> = strings.map(toInt)
let first10: List<Result<Int>> = numbers.take(10)
let result: Result<List<Int>> = sequence(first10) // List<Result<...>> -> Result<List<...>>
do {
  let mapped: List<Int> = try result
  ...
} catch let error {
  ...
}

If Result is more flexible than throws, why do we need throws? Handling Results manually with manual propagation costs more. We should have a way to handle errors with automatic propagation.

So we need both throws and Result.

Result<Value> vs Result<Value, Error>

I know it is discussed which of untyped throws and typed throws are better. If typed throws is accepted, Result<Value, Error> should be provided instead of Result<Value>

However the proposal about typed throws has been left for several months. I'm not sure if the discussion is continued. So I started this thread with Result<Value>. And even if typed throws is accepted, we just need to change Result<Value> to Result<Value, Error>. The discussion for throws as returning a Result can be applied for typed throws and Result<Value, Error> as it is.

When forgets try

If we forget to write try, what will happen? This is a downside of throws as returning a Result.

Even if we forget to write try, it can raise an compilation error with throws as returning a Result. However the error sites are confusing and nonintuitive.

func toInt(x: String) throws -> Int { ... } // -> Result<Int>

let a = toInt(aString) // Compilation error here with Swift 2.X
let b = toInt(bString)
let sum = a + b // Compilation error here with `throws` as returning a `Result`

I think it can be eased by improved error messages.

With side effects

If a function has side effects, its error should not be ignored implicitly.

func update(x: Int) throws { ... } // -> Result<()>

update(42) // No compilation error => dangerous!!

So I think throws should add the @warn_unused_result attribute to the function automatically. If we had a kind of @error_unused_result attribute, it would be better.

update(42) // Warning or Error
_ = update(42) // Ignores error explicitly

// Manual propagation
switch update(42) {
  case .Success:
    ...
  case .Failure(error):
    ...
}

// Automatic propagation
do {
  try update(42)
  ...
} catch let error {
  ...
}

Why not Either, union types nor tuples

Result is preferred to Either as discussed on this thread.

Either is a tagged union. However its tags are meaningless: Left and Right. I think it is much better to have something like union types in Ceylon and some other languages than to have Either.

However union types make a significant impact on the type system. Subtyping gets much more complicated. We should also think about intersection types in addition to uniton types. If we had union types, I think it would be better that Optional<Foo> is a sugar of Foo|Nil like in Ceylon. Changing all of them is not practical.

How about tuples? Tuples like (Value?, Error?) is an easy way. However it results four cases: (value, nil), (nil, error), (value, error) and (nil, nil). We don't need the last two.

Therefore I think Result is the best way.

Return value of do

We easily think of a return value of do statement/expression like Haskell's do notation.

func toInt(x: String) throws -> Int { ... } // -> Result<Int>

let sum: Result<Int> = do {
  let a: Int = try toInt("2")
  let b: Int = try toInt("3")
  a + b
} // Result(5)

It can be regarded as a syntactic sugar of nested flatMaps.

However it causes following problems.

  1. It made it impossible to return, break nor continue inside the do statement/expression.
  2. Returning the evaluated value of the last expression in braces is not Swifty.

I think the following can be the alternative.

let sum: Result<Int> = { () throws -> Int in
  let a: Int = try toInt("2")
  let b: Int = try toInt("3")
  return  a + b
}()

Complication with rethrows

With throws as returning a Result, what should be the type of the following numbers?

func toInt(x: String) throws -> Int { ... } // -> Result<Int>

let numbers: ??? = ["one", "2", "3", "four", "5"].map(toInt)

It can be regarded as both Result<List<Int>> and List<Result<Int>>. I think it should be decided by the declared type of the numbers.

// Both works
let numbers: Result<Array<Int>> = ["one", "2", "3", "four", "5"].map(toInt)
let numbers: Array<Result<Int>> = ["one", "2", "3", "four", "5"].map(toInt)

If the type of the numbers are omitted, what happens? We have some options.

  • Compilation error because of the ambiguous type
  • The default type like that the one for integer literals is Int
    • e.g. If map is marked as rethrows, returns Result<Array<Int>>, and Array<Result<Int>> for the others.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment