The Soundtrack
I tried to write a post about JavaScript Promises using Ciara - Promise, but it didn't work so instead have this genre-agnostic playlist of 10 songs with the title Promise. Don't let my hard work be in vain & have a listen.
The Background
JavaScript and Ruby are both single-threaded programming languages, they can really only do one thing at a time, in a particular order. This also means they're both synchronous. They run in a queue-like way, the first operation or function to get called is the first to be performed before any other operation is performed, this presents a problem the moment you want to do anything that requires multi-tasking. Both languages have workarounds, modules, gems and in-built features that can allow you to write asynchronous code, e.g. JavaScript's Web Workers or background jobs in Ruby. JavaScript also has promises, the topic of today, which Ruby doesn't have an in-built match for at the moment, so I'm going to try my best to recreate what this could look like.
The Promise
It's a commitment to give you something later, it'll either be the thing you ask for or an error but you'll definitely get something.
Essentially, a promise is a returned object to which you attach callbacks, instead of passing callbacks into a function.
-https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises
Callbacks in Ruby
In Ruby, we really mainly deal with callbacks in the context of Rails (or other web framework) when we're manipulating objects during their creation phase. You might have used a before_create: :do_thing
in a model, this is generally what callbacks are in Rails (not necessarily Ruby), and there are a list of them. But using plain old Ruby, you'd have a method that you could pass a block to:
def add_one(n)
total = n + 1
yield total
end
add_one(5) { |total|
puts "the total is #{total}"
}
The callback here is the block we pass to add_one(5)
which is then called with our yield
in the definition of the method. So here we're passing the callback to the method.
Callbacks in JavaScript
Unlike Ruby, JavaScript functions can accept functions as arguments but not blocks which means you'd create dedicated callback functions in a JS context.
function getTotal(t) {
return "the total is ${total}"
}
function addOne(n, callback) {
const t = n + 1;
callback(t);
}
addOne(5, getTotal);
Here we're also passing the callback to the function, similar to the Ruby implementation. This is synchronous since a blocking operation (the addOne
) needs to happen first before the callback can happen.
Implementation
There isn't a native way to write promises in Ruby but just to illustrate the functionality, imagine being able to send data between your controller and view without refreshing the page in Ruby, with no JavaScript. It's the stuff dreams are made of but in the real world we need JavaScript.
I've been working on the Samsung's Global Goals PWA and in this I've had to use promises to interact with Stripe and the Payment Request API. Let's see a real world example of this:
async function fetchPaymentIntentClientSecret(amount){
const fetchedPaymentIntentCS = await fetch(`/fetchPaymentIntent/${amount}`);
const clientSecretObj = await fetchedPaymentIntentCS.json();
return clientSecretObj.clientSecret;
}
fetchPaymentIntentClientSecret(amount).then((clientSecret) => {
confirmPayment(paymentRequest, clientSecret);
}).catch((err) => {
console.log(err);
});
The fetchPaymentIntentClientSecret
function is defined using the keyword async
, in the function we make a call to the server using await
and fetch
this call then gives us back some data which we return. The async
and await
functions are important here:
The
async
andawait
keywords enable asynchronous, promise-based behaviour to be written in a asynchronous style, avoiding the need to explicitly configure promise chains.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
The function could also be written like this:
function fetchPaymentIntentClientSecret(amount){
return fetch(`/fetchPaymentIntent/${amount}`)
.then(response => response.json()) // response.json also returns a promise since it has to wait for the response to finish before it can parse it
.then(clientSecretObj => clientSecretObj.clientSecret); // extract the thing you need from the response
}
const fetchPaymentIntentCSPromise = fetchPaymentIntentClientSecret(amount)
.then(clientSecret => confirmPayment(paymentRequest, clientSecret));
This means that fetchPaymentIntentClientSecret
actually returns a promise. async
and await
are just syntactic sugar for the promises syntax. Using these keywords together, along with fetch
allows us to make the asynchronous call to the server. So when we actually call the function, because it's a promise, we can chain the callbacks and really take advantage of the asynchronous nature. The clientSecret
is returned from the server and we can pass that to the next function that needs it if the call is successful and if it's not, we can log the error instead.
All without the page being refreshed or modified.
A Note
You might have seen promise syntax that looks like
function myFancyFunc() {
// does something
}
const myFancyFuncPromise = new Promise(myFancyFunc)
and you're wondering why I haven't done that here. Well, the syntax is different if you're working with a promise-based API, which I am. In our example fetch
returns a promise as does response.json
so we need to treat them as such. new Promise
is used to make promises out of async APIs which are not promise based, e.g. the callback based functions we defined earlier.
Why?
Within the context web development, promises are unique to JavaScript in that they're native. Coming from a Ruby background I found them strange, why not just do these things in a background job? But honestly, a small action like retrieving a client secret doesn't need to be done in a job (and probably shouldn't be) and it's probably not the best user experience to reload the page just to get a client secret, especially if the user hasn't triggered it.
Promises can also be quite complex to get your head around, this post is a primer but I'd encourage you to read more:
Top comments (2)
Hi Lola, being a RoR developer myself I also found the concept of Promises very counter-intuitive when I saw it first. I think a good start for Rubyists is to check out ruby-concurrent. They also have async/await.
Thank you! Ruby Concurrent was actually mentioned in my draft and you're right, it's a good library for implementing promise-type & async functionality into Ruby projects.