Reactive programming techniques are becoming more prevalent in the constantly changing JavaScript landscape. This article series hopes to provide a snapshot of where we're at, sharing multiple techniques; variations on a theme. From new languages like Elm to the way Angular 2 has adopted RxJS, there's something for every developer, no matter what they're working with.
This InfoQ article is part of the series "Reactive JavaScript". You can subscribe to receive notifications via RSS.
Introducing reactivity can make life as a JavaScript programmer nicer...but what if you could program in a language built on reactivity?
The Elm programming language is exactly that. Where other approaches to reactivity incrementally improve JavaScript, Elm goes back to the drawing board and rebuilds from scratch. Elm was created not to answer the question “how can JavaScript be better?” but rather to answer the question “what would be the nicest overall developer experience for building Web user interfaces?”
As it turns out, when designing with that goal in mind, you end up with a language that is quite different from JavaScript! In pursuit of this goal, Elm embraces several ideas that JavaScript does not:
- Reactivity as the only system for responding to interaction
- One consistent way to manage effects
- A helpful compiler that rules out many bugs up front
The result is a language that compiles to JavaScript, but has a radically different reputation. It’s common to hear that someone’s production Elm code has never thrown a single runtime exception, not even the dreaded undefined is not a function.
How does Elm achieve this different experience? It all starts with the architecture.
The Elm Architecture
You may have already heard of The Elm Architecture, which inspired Redux and other reactive JavaScript libraries. The idea is to divide your application into three simple pieces:
- Model
- Update
- View
The model is an immutable value that represents the entire application state. update is a function that takes the current model and a message—an immutable value describing desired changes to that model—and returns a revised model. view is a function that accepts the current model and returns a representation of the desired DOM structure.
Elm’s runtime connects these pieces to form a reactive application. When a user clicks a button, there is only one permitted outcome: a message is sent to the update function. This yields a nice separation of concerns: all application logic is implemented through the update function, all rendering logic is implemented through the view function, and all application state is stored in the Model.
The Elm Architecture has a simple approach to modularity. When you want to separate out some rendering logic, you write another view function and have the main view function call it. If your Model gets uncomfortably large, make a smaller Model and nest it within your main Model. If you want an independent component that manages its own state, give it a Model, view, and update, and have its parent Model, view, and update delegate to the child as appropriate.
This radically simple architecture takes some getting used to, but it keeps everything neatly organized as your code base scales. No matter how many helper functions you call, all application state is nested within the main Model, all application logic is nested within the main update function, and all rendering logic is nested within the main view function.
Reacting to global events is also simple. You write a subscriptions function which looks at the current Model, determines which events your application wants to subscribe to—anything from keyboard presses to data arriving from a websocket—and then translates those events into messages that get sent to your update function.
Centralizing logic in model, view, and update like this means that reactivity in Elm involves very little housekeeping. Event listeners and observables can be created, reconfigured, and removed—which means more things to keep track of. There’s nothing like these to track in the Elm architecture, just an optional subscriptions function that sends messages to the same update function you’ve been using all along for DOM events like onClick.
What distinguishes Elm from other compile-to-JS languages—like CoffeeScript, Dart, and ClojureScript—is not only what it adds, but also what it leaves out. This Model-View-Update architecture is not a suggestion in Elm; it’s the only way to write applications! This means every library in Elm’s package repository is built around this idea, and there’s no backlog of alternative rendering strategies to decide between. There’s just one extremely well-supported way to do it.
Managed Effects
One of the most common warnings you’ll see in reactive JavaScript library documentation is “don’t do side effects here.” Elm does not have these warnings, because it supports only managed effects, not side effects—and managed effects do not cause the problems side effects do.
In a managed effects system, rather than performing effects immediately, you describe what you want done using data. A Flux store might make a call that immediately fires off an HTTP request as a side effect, whereas in one of Elm’s Update functions, you would instead return a description of the HTTP request you want done—in addition to your normal model updates, of course. Elm’s runtime will take care of translating these descriptions of HTTP requests into actual HTTP requests.
The crucial difference here is that in a managed effects system, APIs can reliably enforce which functions can return descriptions of effects. For example, the update function can return both a new Model and a description of any effects it wants done...but the view function may return only a description of the DOM it wants. Unintended side effects are ruled out at the API level!
Even better, managed effects bring a great deal of consistency to common questions in the world of reactive JavaScript. Some APIs use Promises, others callbacks, others synchronous side effects...in Elm, there are only Tasks.
Tasks are like callbacks in that instantiating them is harmless. You can instantiate a hundred Tasks that describe HTTP requests, and no network activity will happen—yet. Only once the Task is passed from function to function and handed off to the Elm runtime will it actually be performed. Tasks can be chained together like Promises, and they similarly incorporate first-class error handling; if any Task in the chain fails, the remainder of the chain is not executed, and the entire chain results in that failure value.
A common pain point around Promises is that they can swallow exceptions. Elm’s Tasks do not suffer from this problem, because there are no exceptions in Elm! If an effect can fail, the only way it can fail is through a Task’s built-in failure handling. Elm has no equivalent of try/catch, and no equivalent of throw. It’s Tasks all the way down.
This is possible in Elm because all effects are implemented as Tasks, which means that all failed effects necessarily use Task’s error handling system. In contrast, JavaScript error handling is a mix of exceptions, rejected Promises, and sometimes error arguments being passed to callbacks. Elm’s consistency around effects means that problems like clashing failure mechanisms—such as exceptions and rejected Promises—do not exist.
The Compiler Has Your Back
A saying familiar to Elm programmers is “if it compiles, it generally just works.”
What makes this experience possible is not that Elm’s compiler has mysterious error-hunting powers, but because its job is to enforce guarantees about an intentionally simple architecture. Language-level reactivity and having managed effects instead of side effects rules out a huge number of potential ways for things to go wrong. Many of the remaining pitfalls are things a compiler can verify ahead of time.
One such problem is a mistyped field name. Suppose you intended to write phoneNumber but accidentally wrote phoenNumber instead. Here’s an example of the kind of error message you might see:
This bug will never reach your end users, because this error appears at compile time. Even better, you won’t have to debug it by backtracking through a call stack that ends in a message like phoneNumber is undefined. Elm’s compiler not only identifies the problem up front, it even points you to the offending line number.
One of the nicest parts about this experience is what it means for refactoring. Making large, sweeping changes to a code base inevitably leads to compiler errors—we programmers make plenty of mistakes, after all—but once you’ve worked through the compiler errors...as the saying goes, “it generally just works.” It’s very refreshing! No undefined is not a function, just code that works and doesn’t crash.
Another consequence of this is that you get a gigantic pile of test coverage for free. Elm’s compiler automatically verifies that things are wired up reasonably, replacing the many defensive tests you would otherwise have to write to approach the same level of confidence Elm’s compiler gets you for free. This leaves you writing fewer tests and ending up with more reliable code.
Better still, Elm’s package manager is aware of these guarantees and uses them to enforce semantic versioning automatically. If anyone attempts to publish a package with a breaking API change, the package manager rejects it unless the change includes a bump to the major version number. If you’re curious what API changes took place between any two versions of any packages, you can run something like elm-package diff NoRedInk/elm-rails 2.0.0 3.0.0 to see what changed between versions 2.0.0 and 3.0.0 of that package.
Elm’s JavaScript interoperation system is designed to preserve these guarantees. Rather than sharing code with JavaScript—code that might very well crash—an Elm application communicates with JavaScript code the way it would communicate with a server or Web Worker: by sending data back and forth. The only difference is that instead of transmitting this data over the network, or to and from a Web Worker, Elm transmits the data to and from a different language.
Teams talk of “Elm Land” and “JavaScript Land,” two parts of the code base that play by different rules. In Elm Land, you can relax, confident Elm’s compiler will keep your code from crashing. In JavaScript Land, you’re necessarily less confident—in the back of your mind, you always know there could be a missing null check somewhere that escaped test coverage.
By keeping the two separate, and communicating only through data, Elm can provide a top-shelf reactive programming experience while still having access to the vast ecosystem of JavaScript libraries out there.
Summary
Of all the options available to today’s Web developer, Elm most fully embraces reactivity: at the level of the language itself. Along with a helpful compiler designed to maximize the benefits of a fundamentally reactive design, Elm gets you quite a bit:
- Zero runtime exceptions as a normal experience—”if it compiles, it generally works.”
- A simple application architecture with first-class support from the language
- Consistent, pleasant APIs for chaining effects and handling errors
- Freedom from worrying about problematic side effects that trip up reactivity
- Fewer tests, yet greater reliability thanks to helpful compile-time error messages
Elm is still a relatively new language, but companies like NoRedInk, Prezi, Futurice, Gizra, CircuitHub, and more have already begun putting it to work for them in production applications.
If you’d like to learn more about Elm, here are some useful resources:
- Introduction to Elm - a guide by Evan Czaplicki, creator of Elm
- Building a Live-Validated Signup Form in Elm - a tutorial for JavaScript programmers
- Rethinking All Practices: Building Applications in Elm - a ReactConf talk
- Make the Back-End Team Jealous: Elm in Production - a Strange Loop talk
- Elm in Action, an upcoming book from Manning Publications
About the Author
Richard Feldman is a longtime Web programmer who was an early adopter of first React and now Elm. He's the instructor for the Frontend Masters Elm Workshop and the author of “Elm in Action” from Manning Publications. He works for NoRedInk, where he spends most of his time writing Elm code to help students learn grammar and writing.
Reactive programming techniques are becoming more prevalent in the constantly changing JavaScript landscape. This article series hopes to provide a snapshot of where we're at, sharing multiple techniques; variations on a theme. From new languages like Elm to the way Angular 2 has adopted RxJS, there's something for every developer, no matter what they're working with.
This InfoQ article is part of the series "Reactive JavaScript". You can subscribe to receive notifications via RSS.