JavaScript Promises are a very powerful tool to avoid the callback nesting hell. The following article describes some tips and tricks when working with Promises. For developers with C# async/await knowledge, I will also show the async/await representations. With TypeScript, you can use this async/await syntax today.
Most modern browsers only support JavaScript ES5 which does not provide a native Promise implementation. This is why you need one of the 3rd party Promises frameworks. When choosing a framework, you should check that it follows the Promises/A+ specification or a polyfill. The samples in this article are implemented using the Q Promises library.
Use then
to run code after a Promise has completed
To run code after a Promise has completed, call its then
method and register a fulfilled or failed callback:
function () {
var promise = delay(1000);
promise.then(function() {
// Add success handler code
}, function() {
// Add failed handler code
}).done();
}
The previous code would look like this with async/await:
async function () {
var promise = delay(1000);
try {
await promise;
// Add success handler code
} catch {
// Add failed handler code
}
}
A then
fulfilled callback can return a value or Promise or throw an exception
There are tree ways to complete a then
fulfilled or failed callback:
Return a value
If the fulfilled callback returns a value, then the Promise is immediately resolved with this value after the previous Promise has completed:
function () {
return delay(1000).then(function () {
return "hello world";
});
}
Function implementation with async/await:
async function () {
await delay(1000);
return "hello world";
}
Throw an exception
If the fulfilled callback throws an exception, then the Promise is immediately rejected after the previous Promise has completed:
function () {
return delay(1000).then(function () {
throw "my custom exception";
});
}
Function implementation with async/await:
async function () {
await delay(1000);
throw "my custom exception";
}
Return a new Promise
If the fulfilled callback returns another Promise, then the then
call returns the returned Promise:
function () {
return delay(1000).then(function () {
return http.get("my/path");
});
}
Function implementation with async/await:
async function () {
await delay(1000);
return await http.get("my/data");
}
Either return the Promise or call done
Use then
when the Promise is returned
If your method is called by a Promise-aware callee, then you just return the Promise:
function downloadAndTransform () {
return http.get("my/data").then(function (data) {
return transform(data);
});
}
Use done
when the Promise is not returned
(This section only applies if your Promise implementation provides a done
method)
However, if you don’t return the Promise, you need to call done
. Usually, you only need to call done
in a event callback whose caller does not expect a Promise but void
:
function () {
$("#target").click(function () {
http.get("hello/message").done(function(message) {
alert(message);
});
});
}
This way, if an exception occurs somewhere in the then
-callstack, the exception is finally thrown on the UI thread and thus visible in the console. It is not allowed to return a value or Promise in one of the done()
callbacks because the function will not return a continuation Promise.
Use catch
instead of then(null, rejected)
Lets have a look at the following function which loads an HTTP resource, transforms it and posts it back:
function loadTransformAndSave() {
return http.get("my/path").then(function (data) {
var data = transform(data);
return http.post("my/path", data);
}).then(function () {
alert("Data transformed and saved. ");
}).catch(function (reason) {
alert("Exception: " + reason);
});
}
The then
fulfilled callbacks are skipped in case of a previous rejection. In the sample, I used the catch(rejected)
method which most Promises frameworks provide as a replacement for then(null, rejected)
. The catch
method has only one parameter: A callback which is called when a previous Promise got rejected.
To better understand the previous code, just have a look a the async/await representation of the function:
async function loadTransformAndSave() {
try {
var data = await http.get("my/path");
data = transform(data);
await http.post("my/path", data);
alert("Data transformed and saved. ");
} catch(reason) {
alert("Exception: " + reason);
}
}
Using catch
instead of then(null, rejected)
makes your code much easier to read.
The call to catch
returns a resolved Promise
The call to catch
(or then(null, rejected)
) catches an exception and returns a new fulfilled or rejected Promise. An example:
function loadWithFallbackValue() {
return http.get("my/path").catch(function (reason) {
return "No data found";
}).then(function (data) {
alert(data);
});
}
The async/await representation of the previous code:
async function loadWithFallbackValue() {
var result = undefined;
try {
result = await http.get("my/path");
} catch (reason) {
result = "No data found";
}
alert(result);
}
An async function must always return a Promise
If a function is asynchronous, it must always return a Promise even if the function returns synchronously. This way the caller of the function has always a then
function to chain the Promise. Have a look a the following function:
function getPersonById(id) {
if (id === undefined || id === null)
return Promise.reject(new Error("No id given. "));
return http.get("/Person/" + id);
}
As you can see, the call Promise.reject(exception)
creates an already completed but rejected Promise. If you need a completed and resolved Promise, just call Promise.resolve(data)
.
Avoid nesting in callbacks
When you chain multiple Promises, you may be tempted to return a Promise chain inside a then
callback:
return askValue().then(function (value) {
return a(value);
}).then(function (value) {
return b.then(function (value) {
return c(value);
});
}).then(function (value) {
return d(value);
});
I consider this bad pracise as you should always directly return the Promise, and react to its completion one level deeper:
return askValue().then(function (value) {
return a(value);
}).then(function (value) {
return b(value);
}).then(function (value) {
return c(value);
}).then(function (value) {
return d(value);
});
With async/await, the previous chain will be written as:
var value = await askValue();
value = await a(value);
value = await b(value);
value = await c(value);
return await d(value);
Use Promise control flow helper functions
You should use the available flow helper functions to work with Promises:
Promise.all
Wait for multiple Promises:
var p1 = http.get("my/first/path");
var p2 = http.get("my/second/path");
Promise.all([p1, p2]).then(function (values) {
processValues(values);
});
With async/await, the previous code will be written as:
var p1 = http.get("my/first/path");
var p2 = http.get("my/second/path");
var results = await Promise.all([p1, p2]);
processValues(values);
Use Promises with 3rd-party libraries
Wrap JQuery Promises
If you use JQuery for HTTP calls, you have convert the JQuery Promise into a “native” Promise instance. This can be achieved in the following way:
var promise = new Promise(function (reject, resolve) {
$.ajax({ ... }).done(function (data) {
resolve(data);
}).fail(function (xhr) {
reject(xhr);
});
});
The specification
Now that you have seen some basic samples, you should read the Promises/A+ specification (it is pretty short).
Rico Suter
SOFTWARE ENGINEERING
EDIT
Best Practices async/await JavaScript Promise TypeScript