Directives as Execution Boundaries, Not JavaScript Features

- KinfeMichael Tariku

10 reads.

All explanations in this article are based on current framework implementations and may vary across different platforms and future versions.

Lately on X, I've been watching an interesting debate unfold , notably between Theo and Tanner (TanStack) , around JavaScript directives like:

"use workflow"

"use step"

versus more explicit APIs like:

workflow(myWorkflowFn)

At first glance, this debate feels stylistic. But it really isn't. It's about where execution boundaries live, how much the platform should surface, and who owns determinism.

(And yes, I'm here for the drama. This is better than most reality TV.)

My take: these directives are not JavaScript features, not beginner ergonomics, and definitely not something you should think of like "use strict". They are compile-time, bundle-level boundary markers , and that distinction matters.


Directives Are Compile-Time Execution Boundaries

The key thing people miss is this:

You only get to use directives when you need execution isolation.

These directives exist because the runtime cannot infer isolation purely from function calls.

When you write:

"use workflow"

export async function myWorkflow() {
  // ...
}

you are not instructing JavaScript.

You are instructing the bundler and the platform.

This is the same class of hack as "use client" in React , a bundling directive that changes where and how code is shipped and executed.

  • "use client" → ships code to the browser

  • "use workflow" → extracts code into a durable, replayable execution sandbox

  • "use step" → marks side-effect boundaries inside deterministic execution

This has nothing to do with syntax sugar. This is code partitioning.

Think of it like airport security: you can't just walk through with a suspicious function. You need to declare your intentions upfront, or the bundler will make you empty your pockets and explain why you're carrying a side effect.


Why Function Wrappers Are Not Enough

The alternative pattern looks like this:

workflow(async () => {
  step(async () => {
    // side effect
  })
})

This looks cleaner, but it hides an important problem:

Wrappers only exist at runtime. Directives exist at build time.

With function wrappers:

  • The bundler can't reliably split execution

  • The platform can't statically extract steps

  • Determinism is enforced by convention, not structure

(And we all know how well "by convention" works. It's like trusting your roommate to do the dishes. Technically possible, but you'll find dirty plates in the sink at 2 AM.)

With directives:

  • The bundler can isolate code before execution

  • The platform can generate execution graphs

  • Context can be reconstructed deterministically on replay

Once you need replay, recovery, or sandbox rehydration, wrappers stop scaling.

At that point, you're basically asking a runtime wrapper to do the job of a build-time partitioner. It's like asking a bouncer to also be the architect who designed the club. Sure, they're both at the door, but one of them should have been involved earlier.


Determinism Is the Real Requirement

This is where Vercel's design makes sense.

Vercel workflows rely on deterministic replay:

  • If a workflow crashes halfway

  • Vercel recreates the execution context

  • Previously completed steps are replayed from cached output

  • Only unfinished steps are re-executed

That only works if:

  1. The workflow itself is deterministic

  2. Side effects are explicitly isolated

  3. The platform can reconstruct the execution graph

This is why "use step" exists.

It's a non-deterministic escape hatch inside a deterministic system.

Trying to achieve this purely with wrappers leads to:

  • Repetitive boilerplate

  • Manual idempotency guards

  • Ugly caching logic per step

  • Human error (the most reliable failure mode)

The directive approach makes determinism the default, not an opt-in discipline.

Because let's be honest: if determinism were opt-in, most of us would forget to opt in, and then we'd be debugging why our workflows are non-deterministic at 3 AM. I've been there. It's not fun.


This Is Not "use strict"

I've seen people compare this to "use strict" and that comparison is… wrong.

Like, "comparing apples to a compiler flag" wrong.

"use strict":

  • Is a language semantic toggle

  • Affects runtime behavior

  • Is mostly invisible to app developers now (because we all use it and forgot it exists, like that one friend who moved away but you still have in your contacts)

Workflow directives:

  • Do not change JS semantics

  • Are not interpreted by the JS engine

  • Exist purely for tooling + platform orchestration

They are closer to:

  • "use client"

  • "use server"

  • "edge"

  • "deno" / "bun" conditional entry points

These are for people who understand how runtimes, bundlers, and execution environments interact , not for beginners.

And that's fine. Not everything needs to be beginner-friendly. Some things should make you question your life choices. It builds character.


Directives as a Clearer Context Boundary

One thing directives do better than wrappers is making context boundaries explicit.

When I see:

"use workflow"

I immediately know:

  • This code runs in a different execution model

  • It is replayable

  • It is isolated from request lifetimes

  • It may be resumed hours or days later

A wrapper does not communicate that as clearly , especially once abstractions stack.

This matters when:

  • Reading unfamiliar code (which is 90% of your job if you work in a team)

  • Debugging replay issues (which will happen at the worst possible time)

  • Reasoning about side effects (because side effects are like plot holes in a movie , you notice them when you're trying to explain the story)

  • Designing platform-level guarantees (where "it works on my machine" becomes "it works in my execution boundary, I think")


TanStack's Take (And Where I Agree)

TanStack's blog frames this as an execution boundary problem, not a syntax debate , and that's exactly right.

Directives are a way for frameworks to say:

"This code crosses an execution boundary the platform cares about."

Where I slightly diverge is this:

  • Function wrappers are still useful

  • Directives don't replace them

  • They anchor them

You can absolutely combine:

  • Directives for bundling + isolation

  • Wrappers for runtime ergonomics

But without directives, wrappers alone cannot give you:

  • Static extraction

  • Durable replay

  • Deterministic orchestration


Why Surfacing This Is the Right Call

Some people argue that this is "too much platform leaking."

I disagree.

Once you are building durable systems, the platform must leak , because pretending execution is linear and ephemeral is the real abstraction leak.

It's like trying to hide the fact that your house has plumbing. Sure, you could pretend water just magically appears in your sink, but when the pipes burst, you'll wish you knew where they were.

Vercel surfacing this at the directive level is actually an honest API:

  • It tells you when you leave normal JS execution

  • It tells tooling where boundaries exist

  • It avoids magic inference that breaks under scale

That's a good tradeoff.


Final Thought

This debate isn't about syntax preference.

It's about whether:

  • Determinism is enforced structurally or socially

  • Execution boundaries are explicit or implicit

  • Platforms can reason about your code before it runs

Directives are not pretty.

They're not beginner-friendly.

They're not meant to be.

They're platform control surfaces and once you need durable execution, that's exactly what you want.

Think of them as the "here be dragons" sign on old maps. They're not there to be friendly. They're there to tell you: "Hey, you're about to cross into territory where the normal rules don't apply. Make sure you know what you're doing, or you'll end up debugging replay issues at 2 AM."

And honestly? That's more helpful than pretending everything is simple.

✨ Schedule a call ✨

Let's talk and discuss more about my project and my experience on farming the modern technology

Schedule