Who

DRAFT

Errors and Asynchronisity

An investigation into a handful of programming styles and their impact on the error execution paths. 

First the code. 

(in javascript)

C-style Return Values

function removeRecord(id) {
  let error = remove(id)
  if (error) {
    if (error instanceof NotFoundError) {
      // ignore
    } else {
      // notify the *caller* about the error
      return error
    }
  }
  // ...
}

Callbacks

function removeRecord(id, callback) {
  remove(id, (error, _) => {
    if (error) {
      if (error instanceof NotFoundError) {
        // ignore
      } else {
        // notify the *caller* about the error
        callback(error)
      }
    }
    // ...
  })
}

Async/await

async function removeRecord(id) {
  try {
    await remove(id)
  } catch(error) {
    if (error instanceof NotFoundError) {
      // ignore
    } else {
      // notify the *caller* about the error
      throw error
    }
  }
  // ...
}

RxJS Observables as Reducers

function removeRecord(id) {
  remove$.next(id)
  // the caller is notified about errors that
  // occur *while calling remove.next()*
}

// elsewhere
afterRemove$ = remove$.pipe(
  switchMap(_ => {
    // prevents the "infinite stream" from completing
    return of(_).pipe(
      // captures errors *while calling remove()* into the stream
      switchMap(_ => remove(id)),
      catchError(error => {
        if (error instanceof NotFoundError) {
          // ignore
          return EMPTY
        } else {
          // notify the *observer* about error
          return throwError(error)
        }
      })
      // ...
    )
  })
)

RxJS Observables as Return Values

function removeRecord(id) {
  // capture errors *while calling remove()* into the stream
  return of(null).pipe(
    switchMap(_ => remove(id)),
    catchError(error => {
      if (error instanceof NotFoundError) {
        // ignore
        return EMPTY
      } else {
        // notify the *caller* about errors
        return throwError(error)
      }
    })
    // ...
  )
}

The Differences

  1. C-style Return Values
    • Simple
    • Errors are always available to caller
    • Doesn’t handle asynchronous results (on its own)
  2. Callbacks
    • Functional
    • Errors are always available to caller
    • Sometimes leads to “callback hell”
  3. Async/await
    • Typical
    • Simple
    • Errors are always available to caller
  4. RxJS Observables as Reducers
    • Two error paths
    • Relies on “infinite streams”
    • Only some errors are available to caller
  5. RxJS Observables as Return Values
    • Two error paths
    • No “infinite streams”
    • Errors are always available to caller



Notes:

Async/await unwinds the stack on error, so callers always have access to the errors.  Callbacks not defined by the caller effectively hides the error from the initiator.  This is sometimes appropriate, but not the normal.  Callbacks, especially with RxJS, can thus create a “feed-forward” error path which excludes the caller.