What Is A Promise?

When handling asynchronous operations in JavaScript, such as AJAX requests, or disk IO in Node, the traditional way was to provide callback functions to these to be executed when they complete.  Promises reverse this approach.  When executing asynchronous operations, many functions now return a promise that “resolves” when the operation completes.  We then, optionally, attach a callback function to that.

This may sound confusing or maybe not very useful, so let’s look at an example.  Let’s say we wanted to check if a specific file existed, and delete it if it did.  Then, you wanted to create a new file with new content in its place.  The following code snippet illustrates how to do this without using Promises.   

Note: All some of my examples use TypeScript, but are 100% applicable to JavaScript.

import * as path from 'path';
import * as fs from 'fs';

function replaceOrWriteContent(filePath: string, newContent: string, onComplete: Function): void {
    // Check if the file exists.  Note the callback function to execute on completion.
    fs.exists(filePath, result => {
        // Based on the result, we need to delete the file.  If not, just write it.
        //  Deleting the file is asynchronous.
        if (result) {
            // Delete the file.
            fs.unlink(filePath, () => {
                // Now write the content.
                fs.writeFile(filePath, newContent, 'utf8', (err) => onComplete());
            });
        } else {
            // The file does NOT exists.  Just write the content.
            fs.writeFile(filePath, newContent, 'utf8', (err) => onComplete());
        }
    });
}

Notice how messy it gets as the number of callbacks accumulate. Of particular note, we are forced to make two paths of execution that include the writeFile method. Since unlink is asynchronous, we can’t just execute the writeFile method after unlink, because unlink may not be done deleting the file yet.

Now, let’s look at how we’d do this with Promises.  Notice that, because we return a promise, we don’t need a callback function passed into ours.  The caller of our function will handle that as they need.

import * as path from 'path';
import * as fs from 'fs';
import * as util from 'util';

// Create versions of the file functions that return promises and
//  do not use callbacks through Node's promisify method.
const unlinkP = util.promisify(fs.unlink);
const existsP = util.promisify(fs.exists);
const writeFileP = util.promisify(fs.writeFile);

function replaceOrWriteContent(filePath: string, newContent: string): Promise<void> {
    // Check if the file exists.  Note the callback function to execute on completion.
    return existsP(filePath).then(result => {
        // We want a promise that resolves after we've deleted the file.  If we don't
        //  delete the file, our promise should just resolve immediately.
        let deletePromise = result ? unlinkP(filePath) : Promise.resolve();

        // Wait for the deletePromise to resolve, then write our content.
        return deletePromise.then(() => writeFileP(filePath, newContent, 'utf8'));
    });
}
This is a little easier. Notice that we only have the writeFile call in one code path. We also don’t have to remember to execute a callback. Of course, we do have to remember to return our promises at the end of each call.

Try-Catch Implications

Promises have a special ‘catch’ method, that can be chained with the ‘then’ method (or not). It’s important to understand that wrapping a promise in a try-catch block will not typically catch errors occurring in your Promise. By the time the Promise’s code is executing, it’s typically not inside the try-catch block anymore.

The following code shows the proper way to catch errors using our previous example.

import * as path from 'path';
import * as fs from 'fs';
import * as util from 'util';

// Create versions of the file functions that return promises and
//  do not use callbacks through Node's promisify method.
const unlinkP = util.promisify(fs.unlink);
const existsP = util.promisify(fs.exists);
const writeFileP = util.promisify(fs.writeFile);

function replaceOrWriteContent(filePath: string, newContent: string): Promise<void> {
    // Check if the file exists.  Note the callback function to execute on completion.
    return existsP(filePath).then(result => {
        // We want a promise that resolves after we've deleted the file.  If we don't
        //  delete the file, our promise should just resolve immediately.
        let deletePromise = result ? unlinkP(filePath) : Promise.resolve();

        // Wait for the deletePromise to resolve, then write our content.
        return deletePromise.then(() => writeFileP(filePath, newContent, 'utf8')).catch(err => {
            // This should catch any errors occurring in the deletePromise AND the writeFileP promise, because they are all chained.
            // This would NOT catch anything in the existsP promise, because this catch block is *inside* that one.
        });
    });
}

Async & Await

You may have seen the ‘async’ and ‘await’ keywords in .NET and think they’d be the same here.  I know I did.  While they are similar, they’re not the same.

Anything that returns a promise can use the ‘await’ keyword to indicate that everything after that line must wait for the result of the promise.  The net effect is that all code after a line with ‘await’ in it would be wrapped in the ‘then’ method of the promise.  Further, when assigning the result to a variable, it is automatically unwrapped from its promise.  This allows you to write your asynchronous code the same way you would synchronous code.  Just remember that this is syntactical sugar, and the JavaScript engine is doing the dirty work for you.

Another cool feature about ‘async/await’ is that you can place your code in try-catch blocks like you would with asynchronous code, and it will work as you expect!

Functions that use the ‘await’ keyword in its body must be marked ‘async’ in its declaration, and must return a Promise.  Further, ‘await’ may only be used in a function, so you can’t use it at the file level.

Here is our Promise example using ‘async’ and ‘await’.

import * as path from 'path';
import * as fs from 'fs';
import * as util from 'util';

// Create versions of our file functions that return promises and
//  do not use callbacks.
const unlinkP = util.promisify(fs.unlink);
const existsP = util.promisify(fs.exists);
const writeFileP = util.promisify(fs.writeFile);

async function replaceOrWriteContent(filePath: string, newContent: string): Promise<void> {
    try {
        // Delete the file if it exists.
        if (await existsP(filePath)) {
            // Delete the file.
            await unlinkP(filePath);
        }
    
        // Write the content.
        await writeFileP(filePath, newContent, 'utf8');
        
    } catch (err) {
        // Do something here.  This WILL catch errors occurring in your promises too!
    }
}

Creating Promises

A lot of times, the term ‘asynchronous’ is mistakenly thought of as something run on another thread – even if we don’t control that thread. When we think of asynchronous code though, it may not be of some mysterious origin. We handle asynchronous events all the time when a user presses a button on our page or interacts with a control. Just like any other asynchronous scenario, we can create a promise to respond to a button press too.

The following example illustrates how to wrap a button click in a Promise.  I should point out again that Promises resolve once and only once, so using them for event handlers are less practical.  However, say you have a popup control that’s always created on the fly, then it would make sense to provide a Promise that resolves when it’s closed.

Here, we have a dialog that sits over our main content.  When the user presses the “Close” button, it will hide the dialog. 

The Promise constructor takes a function parameter that takes a ‘resolve’ and ‘reject’ function as arguments.  When the conditions of the promise are met, call the resolve method.  The ‘resolve’ method can be called with or with out a parameter, which will be returned by the promise.  Not illustrated in this example is that the ‘reject’ method is how you throw an error.  It also takes a parameter, which should contain your error details.  If you’re not sure what those are, it would be the same thing you’d use in your throw statement, which is commonly a string describing the error, but it could be anything.

index.js

// Create a promise that resolves when the button is clicked.
const closeClicked = new Promise((resolve, reject) => {
    // Get the button from the UI.
    const button = document.getElementById('close-dialog-button');

    // This resolves the promise, and triggers it's 'then' method to fire.
    button.onclick = () => resolve();
});

// When the promise resolves (from the user clicking the button),
//  we want to close the dialog.
closeClicked.then(() => {
    // Get the dialog div from the page.
    let outputDiv = document.getElementById('dialog');
    // Hide it.
    outputDiv.style.display = 'none';
});

index.html

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>Blank Template</title>
    <meta name="description" content="Some blank page">
    <meta name="author" content="Richard Olson">
    <link rel="stylesheet" href="index.css">
</head>

<body>
    <div id="page-content">
        This is some content on our page. We need to just fill up space, so we can
        see the dialog floating over it. This example illustrates how Promises can be used
        to interact with a button click.
    </div>
    <div id="dialog">
        <div id="dialog-header">Dialog Header</div>
        This is an example dialog. Click the close button
        to close it. We only need it to fire once, because after it's
        closed, we don't need the button again.
        <div id="dialog-footer">
            <button id="close-dialog-button">Close</button>
        </div>
    </div>
    <script src="index.js"></script>
</body>

</html>

index.css

body {
    position: relative;
    margin: 0;

    background-color: lightgray;
}

#page-content {
    max-width: 600px;
    height: 100vh;

    background-color: white;

    padding: 40px 10px;

    margin-left: auto;
    margin-right: auto;

    border-left: 1px solid black;
    border-right: 1px solid black;
}

#dialog {
    display: block;
    position: absolute;

    background-color: aliceblue;
    border: 2px solid black;

    padding: 5px;
    margin: 0;

    width: 200px;

    margin-left: auto;
    margin-right: auto;

    left: calc(100vw / 2 - 100px);
    top: 30px;

    box-shadow: black 3px 3px 8px;
}

#dialog-header {
    font-weight: bolder;
    font-size: 1.25em;
    margin-bottom: 10px;
}

#dialog-footer {
    margin-top: 10px;
}

JavaScript is Not Multi-Threaded!

Finally, I need to point out a misconception I had when I first learned about promises.  Anything can be wrapped in a Promise using the technique I described above.  Wrapping something in a Promise does not mean it runs on another thread though.  JavaScript itself is not a multi-threaded language.  There are some features of the browser and in Node that allow you to perform tasks on other threads, but placing code in a promise does not make it run in a separate thread – or even asynchronously.  They will simply resolve inline if there are no asynchronous calls performed within it.