Broken Promises
Introduction to Alternative Async Primitives
Promise History
# PRESENTING BROKEN PROMISES
- The desire for a new async primitive stemmed from the only previous known technique: callbacks
- This shaped the design of Promises, for good or bad (callback hell)
- It's not the only reason for strange behavior
- Other reasons: keeping the implementation simple so "anyone" can learn, dynamically typed (no types), terminology, Promises vs Thenables, backwards/ecosystem compatibility
- It's also entirely possible we are missing the point of promises (for async to look and behave as sync)
Promise History: Fantasy Land
# PRESENTING BROKEN PROMISES
- Brian Mckenna's Proposal in terms of fp-ts would have been to add
- Promise.of(value) --> lifting any value into the context of a Promise, a convenience function
- Promise.chain(Promise(value)) --> flattening a Promise(Promise(value)) to just Promise(value), distinguishing the map and flatMap behaviors that currently exist in Promises now
- (Nice to have) Make Promise.onRejected method separate
- (Nice to have) A Promise.done method --> To make it lazy
- From this we get applicative for free (parallelism), aka Promise.all
Promise History: Fantasy Land
# PRESENTING BROKEN PROMISES
Fantasy Land: Laziness
# PRESENTING BROKEN PROMISES
- Promises are eager instead of lazy, making them slightly less ergonomic
const promise = new Promise((resolve, reject) => { console.log(value) })
// promise is not reusable because it's already been called!
const promiseLazy = () => new Promise((resolve, reject) => { console.log(value)})
// wrapping a promise in a function is just a promise getter
// if we allow promises to lazy we can
const promiseAPlusPlus = fetchButBetter('api.cool/123/details').then(parse).then(map)
promiseAPlusPlus.run('adam#123')
// Alternatively Brian McKenna
promiseAPlusPlus.done()
Fantasy Land: Common Interfaces
# PRESENTING BROKEN PROMISES
- Unlock ease of development for the developer by providing an API they are already familiar with (map, flatMap, etc.)
- Remove overload of .then being map + flatMap, unlocking applicative (parallel)
// Assumes promises are lazy
const query = clientDB
.map(fnMapValue) // map over value like we would with arrays
.flatMap(value => anotherAction(value)) // flatten a value like we would arrays
// Run the query
query.run('connection123')
const query = clientDB('connection123')
.map(fnMapValue) // map over value like we would with arrays
.flatMap(value => anotherPromise(value)) // flatten a value like we would arrays
// Run the query
query.done()
Fantasy Land: Better Error Handling
# PRESENTING BROKEN PROMISES
- Exceptions and Promise.reject behave the same
- Exceptions are not explicit errors and shouldn't be settled, they should crash because they are unexpected behavior
- Promise.reject is explicit and should be handled
// If a promise has an explicit error to be handled
const getDBData = (id) => {
db.conncet()
return db.query(id)
}
getDBData
.onRejected((error) => {
switch error {
// a more useful switch to handle different errors
}
})
.done(res => {}) // uncaught exceptions prevent the Promise from settling
Real World: Task
# PRESENTING BROKEN PROMISES
- A Task is an async effect that can't fail such as IO (writing to a file, etc.)
// Tasks are lazy by default so they can be reused!
// They use Promises under the hood, hence the await (or use .then)
const ioTask = Task.of(1)
await ioTask() // can never fail and returns 1
// or maybe something more practical, also can be resused
// we get common methods for free like map and chain
const result = pipe(
ioTask,
T.map(n => n + 1), // map is array.map
T.chain(n => T.of(n + 2)) // chain is array.flatMap
)
await result() // returns 4
Real World: TaskEither
# PRESENTING BROKEN PROMISES
- A TaskEither is an async effect that can fail such as reading from a DB or making an API call
// Under the hood just a Task that returns an Either
const successOnly = TE.right(1)
const failOnly = TE.left(2)
const result = pipe(
successOnly
TE.chain(n => failOnly()),
)
await result() // E.left(2)
// Error handling is explicit if you want to get the result
await result()
.fold(
error => console.log(error),
success => console.log(success)
)
Real World: Task + TaskEither
# PRESENTING BROKEN PROMISES
- Laziness === resuable
- Explicit handling of errors when there are errors (Task vs TaskEither)
- A common set of utility functions that we know and don't have to recreate
- Explicitness of behavior instead of function overloading
- Fantasy Land -> Real World
References
# PRESENTING BROKEN PROMISES
- JavaScript promises: The History
- States and Fates
- Promises/A+ Spec
- Category Theory for Promises/A+
- Incorporate monads and category theory #94
- You're Missing the Point of Promises
- Promises are not neutral enough
- Promises/A+ Considered Harmful (Exception Handling)
- Promise - JavaScript | MDN (Thenables)
- Broken Promises
- Fantasy Land
Code
By Adam Recvlohe
Code
A talk about the history of Promises in JS and what fp-ts does to improve them
- 193