Boris Cherny's Blog

On Contagion

September 08, 2019

If you have a tree with a node that has a property P, and all of its parents also need to have property P, then P is contagious.

When can that happen?

Concretely:

async function a() {
  // a has to be async to await b()
  await b()
}

async function b() {
  // b has to be async to await c()
  await c()
}

As a programmer, that seems bad. Bubbling up breaks encapsulation and makes things harder to compose. If I have a nice application and need to mark a function deep in the app async, why do I need to update all of its callers to be async too? Why should a parent know about its grandchild?

For good reason. At runtime, some contagious things are special:

We want to model these runtime behaviors in our language:

In some languages, all of these are captured in a function’s return type. The idea is that these aren’t implementation details – consumers really should know about them. These languages model runtime behavior using types.

But, you sometimes want a trap door:

These are ways to avoid contagion.

What unites all of those? They’re effects, that different languages model differently. Some languages keep them implicit (like throw in languages that don’t have throws clauses), some make them explicit (the State monad).

There’s nothing inherently contagious about these things, as evidenced by the languages that support trap doors for them. It’s up to language designers to say “this thing should be contagious” or “this thing shouldn’t”.

What should be contagious, but isn’t? What is contagious, but shouldn’t be?