Hello and welcome to all my new subscribers! You’re reading Ben’s Guide to Software Development, the newsletter about the views I use to make sense of software, and the techniques I use to change it.
In the last few posts, I’ve been writing about techniques for testing around effects. As I was struggling to write the next post in the series, I realized I hadn’t covered a prerequisite concept, which seemed to warrant a post of its own. Today, therefore, we’ll be taking a (hopefully brief) break from the series to discuss the distinction between static and dynamic function calls.

Definitions
A statically resolvable function call (henceforth, “static call”) is one that runs the same code on every invocation. You can figure out what code that is by reading the codebase. Here’s an example:
const foo = function() {
return bar() + 1;
};
const bar = function() {
return 1;
};
The call to bar
is statically resolvable. We know exactly what function will be invoked when foo
calls bar
. The function foo
will thus always return 2. There is nothing the program can do at runtime that will change this.
A dynamic function call is one where we can’t predict exactly what code will be executed, even if we read all the code in the system. In fact, a different function might be executed on each invocation. Here’s an example:
function attempt(f) {
try {
return f();
} catch {
return null;
}
}
The attempt
function receives another function f
as an argument. It tries to run f
, and if that throws an error, it returns null.
What does f
do? It depends on what function gets passed in when attempt
is called. We cannot say, concretely, what attempt
will do unless we know the value of f
. And the value of f
is only determined at runtime.
What’s the point?
What I want you to take away from this post is:
Static calls make the execution of code easier to follow, at the expense of testability, observability, and (often) development speed. Dynamic calls make the opposite tradeoff. The healthiest codebases I’ve worked in use static calls most of the time, with dynamic calls reserved for significant boundaries, e.g. those between concerns or architectural layers.
In dynamic languages like JavaScript, the difference between static and dynamic function calls is largely in your head: almost every call in JavaScript is technically dynamic. Therefore, the invariants you and your teammates agree on are more important than what the language itself allows in any given situation.
Patterns for Dynamic Calls
Since static calls are the common case, I won’t dwell on them. Instead, we’ll focus on the various ways calls can be made dynamic.
Functions passed as parameters
We already saw an example of this: the parameter f
to the attempt
function above. To drive the point home, here is a function that converts a list of files to base64 and calls a method on a passed-in progressTracker
object. In production, the progress tracker might be a progress bar UI element. In tests, it would probably be a spy or a fake.
async function convertFilesToBase64(paths, progressBar) {
progressBar.setGoal(paths.length);
const promises = paths.map(path =>
fs.readFile(path)
.then(toBase64)
.then(result => fs.writeFile(path, result, "utf8"))
.then(() => progressBar.increment())
);
await Promise.all(promises);
progressBar.finish();
}
Functions returned from functions
Another pattern that’s often associated with dynamic calls is the Factory Method pattern, where a function (the factory) instantiates and returns an object from one of several classes. When you call a method on the returned object, you don’t know which class’s code will run, so the call is dynamic.
In the example below, the downloadPackage
function downloads a specified JavaScript package. Since there are multiple ways to obtain a package (e.g. from npmjs.com, from a Git repository, or from a local directory), there is a separate downloader class for each. The buildDownloader
function parses the package versionConstraint
(a string like “^5.2.3” or “git+ssh://github.com/benchristel/taste#v0.5.0”) and selects the appropriate downloader.
type PackageSpecifier = {
name: string
versionConstraint: string
};
downloadPackage(specifier: PackageSpecifier) {
buildDownloader(specifier).download();
}
function buildDownloader(specifier: PackageSpecifier) {
const {versionConstraint} = specifier;
if (isVersionOnly(versionConstraint)) {
return new NpmDownloader(specifier);
}
if (isGitSshUrl(versionConstraint)) {
return new GitSshDownloader(specifier);
}
if (isHttpUrl(versionConstraint)) {
return new HttpDownloader(specifier);
}
if (isLocalPath(versionConstraint)) {
return new LocalCopier(specifier);
}
throw new UnrecognizedFormatError(versionConstraint);
}
The call to download()
in downloadPackage
is dynamic. It’s not possible to know just by reading the code which downloader will be used at runtime. Any of them could be used.
Functions stored in variables
Calls can also be made dynamic by storing a function in a variable. We saw examples of this in the post on backdoor stubbing, e.g. when we refactored the static sendEmail()
call to a dynamic Emails.send()
call. The dynamicity of the latter allowed us to stub the call in our tests, by swapping out one function for another.
When is a dynamic call not a dynamic call?
Whenever you refer to a variable that contains a function, or call a method on an object, the call is, strictly speaking, dynamic. However, in our mental model of the system, we often ignore this and treat dynamic calls as if they’re static, because we know what code is going to be invoked.
Dynamic in the code, static in your head
Here’s an example of what I’m talking about. The following LoginButton
component accepts a callback function, which it will call when the button is clicked.
(Note that this code uses JSX syntax.)
function LoginButton({logIn, children}) {
return <button
style={primaryButton}
onClick={() => logIn()}
>
{children}
</button>
}
// Usage:
<LoginButton logIn={actions.logIn}>
Log In
</LoginButton>
Because any function could be passed as the value for logIn
, the call is dynamic. However, the name of the parameter strongly suggests that only one function, the actions.logIn
function, should actually be passed. As we’re reading this code, we’ll probably be aware that within LoginButton
, logIn
means actions.logIn
.
This is a missed opportunity, though: LoginButton
isn’t actually doing anything login-specific, so if we’re trying to understand how login works in our app, we shouldn’t have to read the implementation of LoginButton
at all. The less code we have to read, the better. It turns out we can reduce the amount of code we need to read just by changing a couple names.
Dynamic in the code, dynamic in your head
A simple name change is all we need to adjust our mental model of the code. In the example below, I’ve just renamed LoginButton
to PrimaryButton
and logIn
to onClick
.
function PrimaryButton({onClick, children}) {
return <button
style={primaryButton}
onClick={() => onClick()}
>
{children}
</button>
}
// Usage:
<PrimaryButton onClick={actions.logIn}>
Log In
</PrimaryButton>
What a difference that makes! Now, it’s clear from looking at the usage that PrimaryButton is a domain-agnostic, reusable component, totally independent of any login-specific logic. We can now feel much more free to reuse PrimaryButton in new contexts.
With this rename, we’ve converted a static-in-your-head dynamic call to an unabashedly dynamic one. In doing so, we’ve aligned our mental model of the program with the actual structure of the code. This alignment can only make our work easier and our communication clearer.
Naming
The previous example contains an important lesson: often the difference between calls that are static-in-your-head versus dynamic-in-your-head boils down to naming.
When a function has the same name on both sides of an abstraction boundary, calls to it will tend to be static-in-your-head. For example:
<LoginButton logIn={actions.logIn}>
Log In
</LoginButton>
Here, we’re passing a function named logIn
to a parameter named logIn
. That is, the function is called logIn
both inside and outside the LoginButton
component. Since LoginButton
thus seems to be aware of what its callback is supposed to do, passing it a function that didn’t log the user in would feel very wrong.
In order to make calls dynamic-in-your-head, names on opposite sides of a boundary must be at different levels of abstraction. For example:
<PrimaryButton onClick={actions.logIn}>
Log In
</PrimaryButton>
The logIn
function is called onClick
once it’s inside the PrimaryButton
component. Since onClick is more abstract, it highlights the idea that PrimaryButton
makes no assumptions about what the callback might do. The abstract naming lets us reuse the component in different contexts with confidence.
Static versus Dynamic: Pros and Cons
The advantage of static function calls is straightforward: you can read the code and understand what’s going to happen when you run it. This type of predictability is immensely helpful when you’re trying to understand an unfamiliar module: often the first questions I ask when confronted with unfamiliar code are “what does this do” and “who uses it,” and static calls make answering both of those questions easy. By contrast, code where every call is dynamic can feel like a bewildering maze. Jim Coplien once drew attention to this point by referring to dynamic calls as “hypergalactic GOTOs”—implying that anything could be on the other side.
The downside of static calls is that they lock the code together, making it harder to test, reconfigure, and experiment with. When modules are separated by dynamic calls, you can easily isolate them for testing. You can lift UI components out of your app and drop them into a tool like Storybook where you can play with them and see all of their different states. And, often, you can implement new features just by gluing together existing modules in a new configuration. This enables your whole development process to be more experimental: you don’t have to invest as much in an idea before you can demo it to users and find out if it’s really valuable.
The one-line summary is: static calls give you predictable execution and understandable code, while dynamic calls enable lower-risk software development practices.
That said, not all dynamic calls are created equal. Static-in-your-head dynamic calls, introduced only for testing purposes, can make code more confusing. They are what lead to claims that unit testing is a waste of time and test-driven development damages software designs. Suffice it to say, I don’t think those claims hold water if we wield dynamicity judiciously and name our variables at the appropriate level of abstraction. Though that’s a pretty big “if”.
We’ll take up further investigation of these tradeoffs in future posts. For now, happy holidays.