Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Promises That Don't Fail

DZone's Guide to

Promises That Don't Fail

We take a look at a way to write Promises in JavaScript that don't fail, and how this compares to other popular web development languages, like Go and Lua.

· Web Dev Zone ·
Free Resource

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

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, they 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 two 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 asyncawait 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 necessarily 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 asyncawait fans very happy to be free of try/catch.

Pure Functions

Promises help encourage pure functions for asynchronous 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, the 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 and Prior Art

You see in our sureThing example above we return an Object that has three proprties: ok, data, and error. This is just a convention that we follow, taking the 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 asyncawaits, you wouldn't restructure, 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 asyncawait functions without a try/catch, and helping ensure you're creating pure functions.

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

Topics:
web dev ,promises ,javascript ,web application development

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}