npm install --save fpromise
fPromise
is a javascript library for working with promises.
It seeks to resolve three problems with promises:
- Promises have an API which encourages casually dangerous code
- Promises co-mingle rejected promises with unintended native exceptions
- Promises lack a suite of convenient API methods to work with results
(For background, and probably a better explanation about this library, read that article about the problems with promises).
fPromise
solves these issues by adding a layer of abstraction within promises - re-designing promises's two path design (resolved/rejected) into three paths:
a data path, a non-native exception path (ie, for promises rejected by your
own intentions), and a native exception path.
With these three paths, we can have an API which is safe, intentional, convenient, and more readable.
Importantly this abstraction:
- using promises
- leave the promise prototype untouched
- provide a safe API for using them which isn't casually dangerous
- ensures unintentional runtime errors are not handled
- provides utility methods for working with the data
- increases readability (vs try blocks)
- keeps control in main call block (so returns work)
fPromise
works by ensuring only native exceptions are ever in the "rejected" lane,
and uses a special wrapper object for the "fulfilled" lane: one which has
convenient methods available on it, and which unboxes to [data, error].
This mean we never have to try/catch with await (a pattern which is less readable and can encourage casually dangerous code) - and that unintentional errors are then are uncaught (like how syncrounous code works).
Instead of using try/catch, we destructure the special object to receive either the data or the non-native issue which occurred.
Pass a promise to the fp
function.
const { fp } = require('fpromise');
const [data, issue] = await fp(Promise.resolve('foo')); // data === foo
const [data, issue] = await fp(Promise.reject('bar')); // issue === bar
const [data, issue] = await fp(Promise.resolve().then(x => undefined())); // throws! good!
If you want to work with the data, await the promise (so it resolves to the Either, and then use those methods).
const { fp } = require('fpromise');
const [data, issue] = (await fp(Promise.resolve('foo'))
.tap(console.log)
.map(d => d.toUpperCase())
// prints foo, data === FOO
const { map, tap } = require('fpromise');
const [data, issue] = await Promise.resolve('foo'))
.then(...tap(console.log))
.then(...map(d => d.toUpperCase()))
// prints foo, data === FOO
// data-access/user.js
const save = user => fp(db.execute(user.getInsertSQL()));
// service/user.js
const save = async data =>
(await save(User(data)))
.tap(getStandardLog('user_creation'))
.map(User.parseUserFromDB)
.itap(logError);
// controllers/user.js
const postHandler = async (userDate, response) => {
const [user, error] = await save(userData);
if (error) {
const errorToCode = { IntegrityError: 422 };
return response.send(errorToCode[error.constructor.name] || 400);
}
response.send(204);
postEmailToMailChimp(user.email).tapError(logError);
};
If we wanted to use the more functional approach, no need for initially wrapping the promise:
// data-access/user.js
const save = user => db.execute(user.getInsertSQL();
// service/user.js
const save = data => save(data)
.then(...tap(getStandardLog('user_creation')))
.then(...map(User.parseUserFromDB))
.then(...itap(logError))
// controllers/user.js
const postHandler = async (userDate, response) => {
const [user, error] = await save(userData);
// ...
}
If we want to move even further in the functional direction, we could:
// data-access/user.js
const save = user => db.execute(user.getInsertSQL();
// service/user.js
const save = data => save(data)
.then(...tap(getStandardLog('user_creation')))
.then(...map(User.parseUserFromDB))
.then(...itap(logError))
// controllers/user.js
const postHandler = (userDate, response) =>
save(userData).then(...map(
user => //...
error => //...
);
function | explanation / example |
---|---|
fp |
Accepts a promise and return a promise which rejects for native errors or resolves to an Either const [data, issue] = await fp(Promise.resolve('foo')); // data == 'foo' |
Data
utility methods | explanation / example |
---|---|
map |
Accepts a transforming fn and returns a Data with the newly transformed value const [data, issue] = Data('foo').map(x => x.toUpperCase()) // data === FOO |
imap (issue map) |
Accepts a transforming fn, but is a no-op - method targets Issue const [data, issue] = Data('foo').imap(x => x.toUpperCase()) // data === foo |
bmap (both map) |
Accepts two transforming fns, and returns a Data with the value of the first fn's transform const [data, issue] = Data('foo').bmap(x => x.toUpperCase(), y => y.length) // data === FOO |
raw |
Accepts a transforming fn and returns its value (without reboxing in Data) const val = Data('foo').raw(x => x.toUpperCase()) // val === FOO |
iraw (issue raw) |
Accepts a fn, but is a no-op - method targets Issue const val = Data('foo').iraw(x=> x.toUpperCase()) // val === foo |
braw (both raw) |
Accepts two transforming fns, and returns the value of the first fn's transform (without reboxing) const val = Data('foo').braw(x => x.toUpperCase(), y => y.length) // val === FOO |
tap |
Accepts a side effect fn and runs it on the data (fn's return is not used) const [data, issue] = Data('foo').tap(console.log) // foo is printed; data === 'foo' |
itap (issue tap) |
Accepts a side effect fn but is a no-op - method targets Issue const [data, issue] = Data('foo').itap(console.log) // nothing printed, data === 'foo' |
btap (both tap) |
Accepts two side effect fns, and runs the first fn (fn's return is not used) const [data, issue] = Data('foo').btap(console.log, console.warn) // foo is printed; data === 'foo' |
val |
Returns data as first element in array const [data, issue] = Data('foo').val() // data === foo |
isData |
Returns true Data('foo').isData // true |
isIssue |
Returns false Data('foo').isIssue // false |
[Symbol.iterator] |
Yields data and nothing else. This is the reason we can "unbox" Data with array destructing const [data, issue] = Data('foo') // data === 'foo' |
Issue
utility methods | explanation / example |
---|---|
map |
Accepts a transforming fn, but is a no-op - method targets Data const [data, issue] = Issue('bar').imap(x => x.toUpperCase()) // issue === bar |
imap (issue map) |
Accepts a transforming fn and returns an Issue with the newly transformed value const [data, issue] = Issue('bar').imap(x => x.toUpperCase()) // issue === BAR |
bmap (both map) |
Accepts two transforming fns, and returns a Issue with the value of the second fn's transform const [data, issue] = Issue('bar').bmap(x => x.length(), y => y.toUpperCase) // issue === BAR |
raw |
Accepts a fn, but is a no-op - method targets Issue const val = Issue('bar').raw(x=> x.toUpperCase()) // val === bar |
iraw (issue raw) |
Accepts a transforming fn and returns its value (without reboxing in Issue) const val = Issue('bar').iraw(x => x.toUpperCase()) // val === BAR |
braw (both raw) |
Accepts two transforming fns, and returns the value of the second fn's transform (without reboxing) const val = Issue('bar').braw(x => x.length(), y => y.toUpperCase()) // val === BAR |
tap |
Accepts a side effect fn but is a no-op - method targets Issue const [data, issue] = Issue('bar').itap(console.log) // nothing printed, issue === 'bar' |
itap (issue tap) |
Accepts a side effect fn and runs it on the issue (fn's return is not used) const [data, issue] = Issue('bar').itap(console.log) // bar is printed; issue === 'bar' |
btap (both tap) |
Accepts two side effect fns, and runs the second fn (fn's return is not used) const [data, issue] = Issue('bar').btap(console.warn, console.log) // bar is printed; issue === 'bar' |
val |
Returns issue as the second element in array const [data, issue] = Issue('bar').val() // issue === bar |
isData |
Returns false Data('bar').isData // false |
isIssue |
Returns true Data('bar').isIssue // true |
[Symbol.iterator] |
Yields undefined and then data. This is the reason we can "unbox" Issue with array destructing const [data, issue] = Issue('bar') // issue === 'bar' |
Funtional
You would use these if you don't use fp
to wrap the promise.
functions |
---|
map |
imap (issue map) |
bmap (both map) |
raw |
iraw (issue raw) |
braw (both raw) |
tap |
itap (issue tap) |
btap (both tap) |
- https://medium.com/@gunar/async-control-flow-without-exceptions-nor-monads-b19af2acc553
- https://blog.grossman.io/how-to-write-async-await-without-try-catch-blocks-in-javascript/
- http://jessewarden.com/2017/11/easier-error-handling-using-asyncawait.html
- https://medium.freecodecamp.org/avoiding-the-async-await-hell-c77a0fb71c4c
- https://medium.com/@dominic.mayers/async-await-without-promises-725e15e1b639
- https://medium.com/@dominic.mayers/on-one-hand-the-async-await-framework-avoid-the-use-of-callbacks-to-define-the-main-flow-in-812317d19285
- https://dev.to/sadarshannaiynar/capture-error-and-data-in-async-await-without-try-catch-1no2
- https://medium.com/@pyrolistical/the-hard-error-handling-case-made-easy-with-async-await-597fd4b908b1
- https://gist.github.com/woudsma/fe8598b1f41453208f0661f90ecdb98b
- https://gist.github.com/DavidWells/56089265ab613a1f29eabca9fc68a3c6
- https://github.com/gunar/go-for-it
- https://github.com/majgis/catchify
- https://github.com/scopsy/await-to-js
- https://github.com/fluture-js/Fluture
- https://github.com/russellmcc/fantasydo
Known TODOs
-
Actually make the readme good/helpful/clear.
-
Decide about aliases:
Data Issue Both map (dmap) imap bmap (map with two functions) raw (draw) iraw braw (raw with two functions) tap (dtap) itap btap (tap with two functions)
Is it production ready? This library is a version two of library extracted out of code in production at www.kujo.com. Kujo is still on that version 1, though, for now.