Introduction
My co-worker, Jason Kaiser, created a way for Promises not to fail, called sureThing
. It has 3 benefits to your code that we’ll illustrate below including prior art in other programming languages so you know this isn’t some made-up concept.
What is a SureThing?
A sureThing
is a function that wraps a Promise
to ensure it never fails. The return value given to the .then
is an Object letting you know if the operation succeeded or not.
const sureThing = promise =>
promise
.then(data => ({ok: true, data}))
.catch(error => Promise.resolve({ok: false, error}));
Returning a resolved Promise in the .catch
ensures you prevent the error from propagating, and the Promise becomes resolved instead of rejected, what it normally becomes when a Promise receives an Error
or another rejected Promise. Let’s see how this construct can help.
Error Handling
Promise error handling is generally simple: Whether 1 error or 50, nested, or not, synchronous or async, will come out in 1 place inside the .catch
callback.
In long Promise chains, however, or those composed of many different promises, while you don’t have to go looking for errors, you certainly do have to figure out “who caused it”, and that isn’t always clear.
const filterDocuments = files => Promise.resolve(filter(file => file.type === 'document', files))
const loadEntitlments = () => request.get(options)
Promise.all([filterDocuments(theFiles), loadEntitlements()])
.then( ([documents, entitlements]) => ...)
.catch(error => {
/* was it filterDocuments or loadEntitlements who failed? */
})
A sureThing
simplifies error handling when using Promise.all
.
Instead of writing more verbose errors in a Promise, you instead look at the results of the Promise.all
Array in the .then
to determine what happened. Note the lack of .catch
in the below code.
const filterDocuments = files => filter(file => file.type === 'document', files)
const loadEntitlments = () => request.get(options)
Promise.all([
sureThing(filterDocuments(theFiles)),
sureThing(loadEntitlements())
])
.then( ([documentsResult, entitlementsResult]) => ...)
The documentsResult
and entitlementsResult
will tell you if they worked or not by checking the ok
boolean.
Another key feature is that the other Promises are not negatively affected by a sibling failing. All are allowed to resolve.
A real-world scenario of this use case is a search I created at work to query 2 different databases. As long as one worked, we were fine to return to the user the results we found. If we used Promise.all
with normal Promises, a failing Promise would prevent the successful one from allowing it’s search results being returned the user.
Async Await
Those who like to use async
await
for making asynchronous code look more imperative and, for them, thusly easier to read. Often upon learning about error handling using this new syntax, they’ll have a sad moment when they learn they must manually include a try/catch in their async
function. The pro is, they can be more strategic about where to use the try/catch as you don’t necessarely have to do the whole function like the below code if you want more fine grained errors.
const loadAll = async (files) => {
try {
const documents = await filterDocuments(files)
const entitlements = await loadEntitlements()
catch(error) {
return error
}
}
Using sureThing
, you have no need for the .catch
for the Promises and your code starts to look like Go or Elixir:
const loadAll = async (files) => {
const documentsResult = await sureThing(filterDocuments(files))
if(documentsResult.ok === false) {
return Promise.reject(new Error('Documents failed to load'))
}
const entitlementsResult = await sureThing(loadEntitlements())
if(entitlementsResult.ok === false) {
return Promise.reject(new Error('Documents were filetered, but we could not load entitlements.'))
}
}
Still, it might be prudent to keep try/catch because if you are writing imperative code like this, you are apt to create exceptions by accident, and the try/catch will keep you safe unlike Go or Lua’s pcall which have facilities to make all functions work like a sureThing. If you wrote this in a normal Promise, you wouldn’t have to worry about it because a Promise has an implicit try/catch.
This style of coding tends to make async
await
fans very happy to be free of try/catch.
Pure Functions
Promises help encourage pure functions for asynchornous operations. A pure function is a function that will always return the same output with the same inputs and has no side effects. When JavaScript asynchronous first started, callbacks were used. They are noops, meaning a function that returns no value (or undefined
). So they aren’t pure and cause side effects on purpose unless you wrap them.
const loadEntitlements = callback => {
/* do some ajax */
if(works) {
callback(undefined, 'your data')
} else {
callback(new Error('failure'))
}
}
const result = loadEntitlements((error, data) => ...)
/* result is nothing useful */
A returned Promise, however, allows a bunch of improvements. First and foremost, same input always results in the same output: an unresolved Promise.
const result = loadEntitlements()
/* result is a Promise */
Since Promises are a data type that wraps a value, but also follows some Monad laws, we can also compose them and use Promises together, like in the case of Promise.all
.
Return Value & Prior Art
You see in our sureThing
example above we return an Object
that has 3 proprties: ok
, data
, and error
. This is just a convention that we follow taking a lead from the Go, Elixir, and Lua developers. It contains the minimum amount of data needed to easily determine if a function worked or not:
– ok
saying yes or no
– data
containing whatever the Promise resolves to
– error
containing helpful information about why the function failed
In longer async
awaits
, you wouldn’t destructure, but in smaller ones you can like so:
JavaScript:
const { ok, data, error } = await loadEntitlements()
Python (assuming load_entitlements
returns a Tuple like (True, 'your data', None)
ok, data, error = load_entitlements()
Lua:
ok, data, error = pcall(loadEntitlements)
Go (Note the convention in Go is if err != nil
):
data, err := loadEntitlements()
Note Elixir uses pattern matching, so this’ll throw an error, which is actually the Erlang way of “let it crash”. A more pure Elixir way would be to always return an Object that follows having nothing for the error so the matching works Elixir:
{:ok, result} = load_entitlements()
Conclusions
As you can see, using sureThings in your code base can help error handling in Promise.all
, when using async
await
functions without a try/catch, and helping ensure you’re creating pure functions.