Swift 2 Error Handling vs Result<T, E>

Swift 2 Error Handling vs Result<T, E>

Swift 2 (& LLDB) シンポジウム
http://realm.connpass.com/event/16556/

Eac0bf787b5279aca5e699ece096956e?s=128

Yasuhiro Inami

June 28, 2015
Tweet

Transcript

  1. 1.

    Swift 2 Error Handling V.S. Result<T, E> 2015/06/27 Swift 2

    (& LLDB) Symposium Yasuhiro Inami / @inamiy
  2. 2.

    Who? • Yasuhiro Inami / @inamiy • LINE Corp •

    WWDC 2015 Attendee • guard case .Left(let Him) = photo else {
 // It’s me
 throw stone 
 }
  3. 5.

    Error Handling in Objective-C + (id)JSONObjectWithData:(NSData *)data options: (NSJSONReadingOptions)opt error:(NSError

    **)error NSData* jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; id json = [NSJSONSerialization JSONObjectWithData:jsonData options:1 error:NULL]; Method Usage
  4. 7.

    Passing Error Pointer (Obj-C) NSError* error = nil; id json

    = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&error]; if (error) { // do error handling return } // do success handling
  5. 8.

    Async Error Handling (Obj-C) NSURLSessionDataTask* task = [session dataTaskWithRequest:request completionHandler:^(NSData

    *data, NSURLResponse *response, NSError *error) { if (error) { // do error handling return } // do success handling }];
  6. 9.

    or, Exception Handling (Obj-C) @try { // Code that can

    potentially throw an exception } @catch (NSException *exception) { // Handle an exception thrown in the @try block } @finally { // Code that gets executed // whether or not an exception is thrown } … but rarely used in Objective-C
  7. 10.
  8. 11.

    Swift 1 (Either/Result) • Optional<T> + Error • map, flatMap

    … functional • 3rd party: LlamaKit/LlamaKit → antitypical/Result enum Result<T, Error> { case Success(T) case Failure(Error) }
  9. 12.

    Sync + Result func doSync<A, T, E>(arg: A) -> Result<T,

    E> ... let result = doSync(arg) switch result { case .Success(let value): // do success handling case .Failure(let error): // do error handling }
  10. 13.

    Async + Result func doAsync<A, T, E>(arg: A, callback: (Result<T,

    E> -> Void)) ... doAsync(arg) { result in switch result { case .Success(let value): // do success handling case .Failure(let error): // do error handling } }
  11. 15.
  12. 22.
  13. 23.
  14. 24.

    Swift 2 Error Handling • Throwing Function • Returns value,

    or throws error (ErrorType) • Must declare throws / rethrows • Supertype of non-throwing function • Works swiftly with existing Objective-C Cocoa APIs • Can set breakpoint (e.g. br s -E swift -O MyError) • Similar to Java’s Checked Exception (not NSException)
  15. 25.

    Ref: Checked/Unchecked Exceptions • Checked Exception • Error from outside

    of program control & should be recoverable • Forces caller to use do-catch conditional blocks • No unwinding callstack penalties for unhandled errors • Unchecked Exception • Programmer’s mistake & should NOT be recoverable
  16. 28.

    do-try-catch / try! • do-try-catch, not “try-catch” • try expression

    is required for **every ** call of throwing function • or, try! (forced-try) to omit do-catch blocks • catch clause can be used like switch statement • Use defer in replace of “finally”
  17. 30.

    Sequential flow (Result) let result3 = doSync1(arg) .flatMap(doSync2) .flatMap(doSync3) switch

    result3 { case .Success(let value3): // do success handling case .Failure(let error): // do error handling }
  18. 31.

    Sequential flow (Result) let result3 = doSync1(arg) .flatMap(doSync2) .flatMap(doSync3) switch

    result3 { case .Success(let value3): // do success handling case .Failure(let error): // do error handling } Functional All Error types must be same...
  19. 32.

    Sequential flow (Result) let result3 = doSync1(arg) .flatMap(doSync2.mapError {…}) .flatMap(doSync3.mapError

    {…}) switch result3 { case .Success(let value3): // do success handling case .Failure(let error): // do error handling } Error conversion is sometimes required...
  20. 33.

    Sequential flow (Throwing Func) do { let value1 = try

    doSync1(arg) let value2 = try doSync2(value1) let value3 = try doSync3(value2) // do success handling } catch { // do error handling }
  21. 34.

    Sequential flow (Throwing Func) do { let value1 = try

    doSync1(arg) let value2 = try doSync2(value1) let value3 = try doSync3(value2) // do success handling } catch { // do error handling } Imperative Can catch any ErrorTypes
  22. 36.
  23. 38.

    Too loose ErrorType func throwLikeCrazy(x: Int) throws { switch x

    { case 0: throw MyError.XXX case 1: throw YourError.YYY default: throw HisError.ZZZ } }
  24. 39.

    func throwLikeCrazy(x: Int) throws { switch x { case 0:

    throw MyError.XXX case 1: throw YourError.YYY default: throw HisError.ZZZ } } Too loose ErrorType No ErrorType info Can throw many different ErrorTypes
  25. 40.

    do { try throwLikeCrazy(x) } catch { // do error

    handling } Too loose ErrorType Catch… What???
  26. 41.

    Async Issue func doSync<A, R>(arg: A) throws -> R Sync

    pattern func doAsync<A, R>(arg: A, callback: R -> Void) throws Async pattern?
  27. 42.

    Async Issue func doSync<A, R>(arg: A) throws -> R Sync

    pattern func doAsync<A, R>(arg: A, callback: R -> Void) throws Async pattern? Can’t throw asynchronously
  28. 52.

    func doSync<A, R>(arg: A) throws -> R func doSync<A, R>(arg:

    A)() throws -> R ↓ 2-step call for lazy evaluation
  29. 55.

    Sync + Lazy Throwing let result = doSync(arg) do {

    let value = try result() // do success handling } catch { // do error handling }
  30. 57.

    Async + Lazy Throwing doAsync(arg) { result in do {

    let value = try result() // do success handling } catch { // do error handling } }
  31. 59.
  32. 60.

    Issues in Lazy Throwing • try result() is ugly •

    No `()`, please… • Ideally, it’s better if we can try Result<T, E> enum rather than try throwing function • Too verbose for sync task & can’t throw immediately • Use lazy throwing for async only? 
 (but it will lack consistency…)
  33. 63.
  34. 64.

    Issues in Result<T, E> • Result<T, E> is just an

    Enum • Same weight for both success value and error • No compiler support (breakpoint, do-catch) • Too easy to ignore errors • var value: T? … Result → Optional conversion • map() / flatMap() … using success value without checking error
  35. 66.
  36. 67.

    Error Handling in Rust // before fn write_info(info: &Info) ->

    io::Result<()> { let mut file = File::create("list.txt").unwrap(); if let Err(e) = writeln!(&mut file, "name: {}", info.name) { return Err(e) } if let Err(e) = writeln!(&mut file, "age: {}", info.age) { return Err(e) } return Ok(()); }
  37. 68.

    Error Handling in Rust // after fn write_info(info: &Info) ->

    io::Result<()> { let mut file = try!(File::create("list.txt")); // Early return on error try!(writeln!(&mut file, "name: {}", info.name)); try!(writeln!(&mut file, "age: {}", info.age)); Ok(()) }
  38. 71.

    func doSomething<T2, E2>() -> Result<T2, E2> { let result: Result<T1,

    E1> = ... let value = try result let value: T1 switch result { case .Success(let v1): value = v1 case .Failure(let e1): return .Failure(E2.convertFrom(e1)) } 㱻
  39. 72.

    func doSomething<T2, E2>() -> Result<T2, E2> { let result: Result<T1,

    E1> = ... let value = try result let value: T1 switch result { case .Success(let v1): value = v1 case .Failure(let e1): return .Failure(E2.convertFrom(e1)) } Early-exit with auto-mapping Error! 㱻
  40. 75.
  41. 76.

    Recap • Both throwing function and Result<T, E> have pros

    and cons • “try result” is more important than ”try execution” • Rust’s Error Handling is awesome