Ruby, for the most part, feels like it handles code with pure synchronicity—code reads from the top to the bottom, left to right, executing in an orderly, predictable fashion. Unless it runs into a block, of course—perhaps something like an API call. Then our code grinds to a halt to handle that request.
For example, when relying on external servers, whether they belong to Google or are a Ruby on Rails backend API you built yourself, it doesn’t make sense to put a stop on your local code while waiting for a response. There are many things in this world we cannot control, the speed of an external server response is but one of them.
Asynchronous functionality can feel foreign at first, but when we break down how it works, we find that it’s really very intuitive to how we as humans already process daily tasks.
The much-loved example of this is making a large, complicated meal.
For, say, Thanksigiving, you wouldn’t prepare your meal by putting in your turkey and then waiting for it to cook entirely before tackling the next dish. If each dish were handled like this, at the end of many hours, you’d have a complete, but cold meal.
Promises are essentially a placeholder for a value that the code does not yet have at the time it’s created. It, therefore, allows you to chain handlers that will work with the results success value or failure.
My favorite description of a promise from MDN describes it as feeling “like Schrödinger’s cat in action.” Much like the iconic thought experiment, a promise is nothing more than the idea that a response will be generated eventually, and at the point we receive the Promise, we don’t know the outcome of the response. A promise is both the completion and failure of a request at the same time.
Promises can have one of three states:
- Pending: this is the initial state, showing that the promise has been neither fulfilled nor failed
- Fulfilled: the operation was completed successfully
- Rejected: the operation has failed and comes with an error
Basic Use of a Promise
What we’re talking about when we talk about async functionality is well illustrated by the use of the
fetch function. Fetch functions take one argument—the path the resource you’re trying to fetch.
In the code above, we have a basic example of the use of a
fetch . We’ve declared and assigned a variable that contains the url to which we’ll be making our request. In the body of the function
fetchData , we’re not only calling our fetch—we’re also chaining
.then() functions that are used to handle actions for the settled promise.
We’re telling the code—once the the promise has been fulfilled, parse the response into JSON data, and then log that response.
But let’s also look at exactly how this function is running asynchronously. When we run this code, it will first log in our console “Started Running.” But what will we see logged next? “Finished Running”! This is because while the code has made the fetch request, it continues to move on to the
console.log("Finished Running") on line 12. Then, once the promise has been fulfilled, it will convert the response into JSON, then log it for us to review.
The non-magical magic of asynchronicity is that it allows for multiple things to happen at once. While our code is moving on and logging “Finished Running” it’s still attending to the promise, and once fulfilled it’s moving through our chained
.then() functions using the results of that promise.
We can even chain a
.catch() function which will allow us to error handle if our promise is rejected
It follows one of my personal favorite goals of programming—efficiency!