ArticlesRuby programmingEngineering

JavaScript vs Ruby

Comparing Ruby and JavaScript with some observations and ranting mixed in for good measure.

It's been almost two years since I joined Stripe and started working with Ruby day to day. I have been doing JavaScript for a decade. There are things I like and dislike about both languages, some interesting observations, and a tad bit of venting I'd like to share.

Stripe Ruby and JavaScript

Stripe Ruby is primarily written in conjunction with Sorbet. So much in fact that Stripe Ruby code not written with Sorbet is considered bad code. I've been able to witness and experience Sorbet's evolution of features, tooling, and company adoption. Despite its bugs, I've grown fond of it and probably would not write Ruby without it, even outside Stripe.

At Stripe we have four flavors of JavaScript in production: vanilla, CoffeeScript, Flow, and TypeScript. The majority is written in Flow. Before Stripe I had not used a tack-on-type-system for JavaScript and for front-end projects I've only worked with Flow due to where in existing code I was working. I definitely have not felt pressure to use Flow outside of Stripe.

Fundamental difference

There are a lot of ways to say JavaScript and Ruby are different languages. The one that resonates with me with the most conciseness follows from these two snippets:

return_value = ruby_object.method
js_function = js_object.property
return_value = js_function()

In Ruby, everything is an object and objects receive and respond to messages. In JavaScript, objects have properties, those properties have values, and those values can be functions.

Ruby's consistency in modeling is pleasant to me conceptually but not being able to decouple method from value gives me fewer guarantees. In JavaScript, properties defined with getter/setters suffer from this same problem. For example:

foo = obj.bar

If you are writing good JavaScript code (e.g. no getters), you can know this is a constant time lookup of bar on obj. In Ruby I am calling a method that can do anything, including maybe never finish running.

For concrete disadvantages of Ruby's approach, we can look to Sorbet. One of the most common frictions developers face when starting with Sorbet is understanding "flow sensitivity."

class MyClass
  sig {returns(T.any(Integer, NilClass))}
  def maybe_int
    if rand > 0.5
      1
    else
      nil
    end
  end

  sig {returns(Integer)}
  def my_method
    if maybe_int == nil
      return 0
    end

    maybe_int
  end
end

Sorbet won't allow this because maybe_int can return a different value on each invocation. To narrow this we need to use a local variable to deal in values. Flow sensitivity is covered in more detail in the Sorbet docs.

This example isn't meant to illustrate Ruby is wrong, just that methods are powerful and maps/properties are dumb. In most cases methods should be referentially transparent but Sorbet doesn't model this, which introduces typing overhead JavaScript doesn't have to deal with as much because properties are inert indirection between methods and values (in good code).

Development approach

How I develop something is highly influenced by what it is. If I do follow a standard procedure in development, it's buried in my subconscious. However, variance of work aside, I see a difference when comparing JavaScript and Ruby.

In JavaScript, I write and get the code working, then make Flow happy, then make the tests happy. In Ruby, I write short chunks of code, get those chunks Sorbet-happy, repeat, and then finally make tests happy. Not too different, but why not the same?

My first thought was I'm just way more familiar with JavaScript. I have been grinding around JavaScript's plethora of quirks so long they are baked into my brain interpreter. There's nothing Flow can tell me that I didn't already know about the code I just wrote. I know my code works, and getting Flow to agree with me is in service of long term maintainability. However, I've done enough Ruby to have a pretty decent brain interpreter for it too.

I don't have a clear answer, only miscellaneous threads.

Runtime checks

Sorbet type checks at runtime (unless code is opted out for performance reasons). I have very low confidence in types which aren't exercised in production since people will always find ways to lie to the type system to move faster. Also, in code where types are tacked on, for typed code that's interacting with untyped code being able to say for the past X days/months in production the untyped code honored the typed contract is really powerful.

Thanks to runtime checks I'm not fighting in distinct compile-time and run-time phases, I can focus on getting the code working by running it and Sorbet ensures my assumptions as I go.

Front-end deficiencies

When I'm writing Ruby, I have the advantage of writing in a more closed system. I'm writing code that interacts with other typed code that's roughly consistently written. Achieving optimal code is much more clear cut as non-business logic complexity and structure has been mostly fleshed out at Stripe.

When I'm writing front-end JavaScript, I've gotta make the browser happy. Web APIs are intentionally designed to be inconsistent (wait, they're not you say?) and in the face of all this complexity (saying nothing of browser differences) I'm more inclined to try grinding out a working solution than type check my code to success. All that one-off, most likely dead end experimentation doesn't need types until it works.

I can see types playing a bigger, more helpful role in server-side JavaScript development where one could construct a similarly closed system.

Language features

It's interesting to me that with both JavaScript and Ruby the statement "The language has many features, about only 10% should be used" I agree with. Despite Ruby certainly leaning towards object orientation a lot more, they both support all the programming styles. As such, I manage to get by with my classic data and functions approach just about as well in Ruby as in JavaScript.

Blocks

Ruby blocks are special and don't have a JavaScript (or most other languages) equivalent. Blocks are essentially anonymous functions (first example) which have access to outer scope flow control constructs (second example).

def my_method(x, y, &blk) # Explicit block arguments like &blk here
  yield x + y             # tend to be non-idiomatic in plain Ruby.
  # ^ De-sugars to:       # In order to Sorbet type blocks, we need
  # blk.call(x + y)       # the explicit arg so I always write it.
end

my_method(1, 2) do |sum|
  puts sum # => prints 3
end
def sum_even_numbers(list)
  sum = 0

  list.each do |num| # Like JavaScript's Array::forEach
    if !num.even?
      next # Like JavaScript's continue keyword
    end

    sum += num
  end

  sum
end

While cool to look at, blocks are hard to wrap my head around even now. I only use them to allow callers of my methods to use the familiar do ... end syntax, never really using them for or intending they be used with all the flow control constructs. Ruby offers lambdas which are the real anonymous functions and I reach for those more often.

Variables

Switching between Ruby and JavaScript daily, I've gotten tired of variable declaration keywords in JavaScript:

var x = 0
let y = 0
const z = 0

In Ruby you only need:

x = 0

What x is in the Ruby context is ambiguous in some contexts and cannot be made constant/final. However I use a lot of variables and most are constant so I feel the pain of JS boilerplate a lot more than I see value in the long term readability.

Where Ruby variables get messy is in allowing this:

def my_method(x)
  if x != 4
    y = 0
  end

  y
end

my_method(4) # => returns nil

Ruby local variables suffer from the same issues as JavaScript's var hoisting. At least the JavaScript equivalent sets off alarm bells since var is basically obsolete:

function myMethod(x) {
  if (x !== 4) {
    var y = 0
  }

  return y
}

myMethod(4) // => returns undefined

Expressions

Speaking of less boilerplate, Ruby is much better than JavaScript because most code is expressions instead of statements. A big advantage is explicit returns:

def my_method
  4
end

Most good code returns values so making the common case easy is great.

Even better than the implicit return, conditionals are also expressions:

x =
  if y
    1
  else
    2
  end

In JavaScript (excluding ternaries), you would need to write:

let x
if (y) {
  x = 1
} else {
  x = 2
}

With conditional expressions, we can discount the hoisting issue from the previous section:

def my_method(x)
  y = if x != 4
    0
  end

  y
end

my_method(4) # => returns nil

Ruby case/when (unlike JavaScript's switch statement) and begin/rescue/ensure (try/catch/finally) expressions are also much nicer.

Falsy & type coercion

In JavaScript, for !!x eight values plus a browser quirk evaluate to false. In Ruby, only !!false and !!nil evaluate to false. Ruby blows JavaScript out of the water in being actually understandable.

The same goes for type coercion, Ruby could do a lot of things more implicitly but doesn't. The mental baggage of working in Ruby is much less.

Strings

In JavaScript strings are immutable, in Ruby they are mutable.

"Woah, JavaScript is better at something?" - You

Ruby has a special pragma to make string literals frozen by default and there are plans to eventually make that the default. In practice, string mutability hasn't been a sharp edge because most developers agree mutability is bad and making copies of strings is easy and common.


As languages, Ruby is better than JavaScript. Of course, JavaScript never got by on its language merits so this isn't too surprising. To be clear, Ruby has quicks of its own. Really bad ones, like this one:

def my_method
  4
end

Object.new.my_method # => 4
[].my_method # => 4
1.my_method # => 4

"I see you defined a top-level method, but methods need an object..Geez, where should I put this? What's a sane default? Oh, I know: everywhere."

But the difference between that quirk and the ones above is that JavaScript imposes a constant and pervasive mental overhead to basically everything you do. Ruby sucks in smaller doses.

Wrapping up

This article is a bit less focused than most of my writing. It's fair to say writing this out had the main benefit of just getting it out of my head in an attempted coherent fashion. This is one of those articles that could be a lot longer, but this is enough for me for now.

I hope you either learned something or this kicked up some introspection of your own. Thanks for reading!