DEV Community

Cover image for Promises for Rubyists
Lola for Samsung Internet

Posted on

Promises for Rubyists

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}"
  }
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
    });
Enter fullscreen mode Exit fullscreen mode

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 and await 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));
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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:

Discussion (2)

Collapse
thorstenhirsch profile image
Thorsten Hirsch

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.

Collapse
lolaodelola profile image
Lola Author

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.