Guarded Declarations (const / let … else …)

Guarded Declarations (const / let … else …)

Summary

This proposal allows const and let declarations to optionally include an else clause that executes when the initializer evaluates to null or undefined, and must exit control flow (return / throw / break / continue).

This provides syntax sugar for a common JavaScript pattern:

const value = expr
if (value == null) return

becoming

const value = expr else { return }

The goal is to keep the “happy path” linear while preserving existing JavaScript semantics.

Motivation

JavaScript developers frequently write guard-style code:

const user = session?.user
if (user == null) return
user.doStuff()

This pattern is correct but verbose, inconsistently written, and often repeated. Optional chaining and nullish coalescing help with expressions, but there is no concise syntax for binding + guarding + early exit.

This proposal introduces no new runtime behavior; it is purely syntactic sugar for an existing, well-understood control-flow pattern.

Proposed Syntax

In the initial design, <binding> refers to a single identifier. Destructuring patterns are deferred to future consideration.

To avoid ambiguity with Automatic Semicolon Insertion, implementations may require else to appear on the same line as the declaration.

A const or let declaration with a single initializer may include an else clause:

const <binding> = <expression> else <statement-or-block>
let  <binding> = <expression> else <statement-or-block>

Examples:

const id = await getId() else { return }
const user = session?.user else { throw new Error("Unauthorized") }

for (const item of items) {
  const key = item?.key else { continue }
  process(key)
}

Semantics (Informal)

const x = expr else alt

desugars to:

const x = expr
if (x === null || x === undefined) {
  alt
}
  • The initializer is evaluated exactly once.
  • The check is nullish-only, not truthy/falsy.
  • If execution continues past the declaration, the binding is guaranteed non-nullish.

Non-Fallthrough Requirement

The else clause must complete abruptly:

  • return or throw in functions
  • break or continue in loops (or return/throw)

This ensures that execution cannot proceed with a nullish binding.

At early stages, this proposal intentionally limits validation to simple, statically obvious, abrupt completions. For example:

  • return, throw
  • break / continue where valid
  • a block whose final statement is one of the above

More complex control-flow analysis (e.g., conditionals inside the else block) is intentionally out of scope for initial exploration.

JavaScript vs TypeScript

JavaScript (runtime guarantee)

function f(maybeUser) {
  const user = maybeUser else { return }
  user.doStuff()
}

TypeScript (static narrowing)

function f(maybeUser: User | null | undefined) {
  const user = maybeUser else { return }
  // user is User
  user.doStuff()
}

TypeScript can treat the binding as NonNullable after the declaration using existing control-flow analysis.

Nullish Guards vs Exceptions

The else clause guards nullish values, not exceptions.

// ❌ Object.keys never returns nullish
const keys = Object.keys(obj) else { return }

Instead, guard the input:

const safeObj = obj else { return }
const keys = Object.keys(safeObj)

In an operation may throw, use try/catch or a helper that converts failure into null:

function tryKeys(value) {
  try { return Object.keys(value) }
  catch { return null }
}

const keys = tryKeys(maybeObj) else { return }

Example: Required vs Optional Values

function firstKeyRequiredSecondOptional(maybeObj) {
  const obj = maybeObj else { return null }

  const keys = Object.keys(obj)

  const firstKey = keys[0] else { return null } // required
  const secondKey = keys[1]                     // optional

  return {
    firstKey,
    firstValue: obj[firstKey],
    secondKey,
    secondValue: secondKey != null ? obj[secondKey] : undefined,
  }
}

Restrictions (Initial)

  • Exactly one declarator per declaration
  • const and let only (var excluded)
  • else clause required to be abrupt
  • Destructuring patterns may be considered in a later stage

Alternatives Considered

  • New keywords (guard, unwrap, require)
  • Throw expressions (expr ?? throw)
  • TypeScript assertion helpers

These alternatives either introduce breaking keywords, are expression-only, or do not reduce JavaScript boilerplate.

Related proposals

Declarations in Conditionals

The “Declarations in Conditionals” proposal allows bindings within if/while conditions, which scopes the binding to the conditional statement and its branches. oai_citation:2‡GitHub

This proposal is complementary: it provides a guard-clause form which binds in the enclosing scope and supports linear happy-path code without introducing an additional if block.

For example, the following Declarations-in-Conditionals style code:

if (let data = foo.data) {
  for (let item of data) { /* A */ }
} else {
  /* B */
}

could be written with guarded declarations as:

const data = foo.data else { /* B */ return }
for (const item of data) { /* A */ }
// `data` remains in scope here

Transpiler support

This proposal is intended to be purely syntactic sugar and can be transpiled via a local desugaring transform.

const x = expr else { return }

desugars to

const x = expr
if (x === null || x === undefined) { return }

This enables experimentation in transpilers and tooling even at early stages. Note that proposal syntax is expected to evolve significantly prior to later stages, and should not be relied on for long-term stability in production code.

Open Questions

  1. Should destructuring patterns be allowed?
  2. Should else require a block or allow a single statement?
  3. Should else be required on the same line (ASI concerns)?

I have also posted this information GitHub - Scarafone/proposal-guarded-declarations.

Thank you for reading.

1 Like

Could this be served with do expressions?

const x = expr ?? do { return }
2 Likes

I wonder whether this could be better modeled as an expression that allows return, analogous to throw being usable as an expression.

const value = expr ?? return; // return if expr is nullish
2 Likes

Thanks for pointing out this proposal. It didn’t come up during my research. I will have to check it out more thoroughly and see whether the proposal, as written today, would work for this. I think, syntax aside, the goal is to achieve a cleaner, earlier-exit pattern in the spirit of what async/await did for promises.

Hmm… so trying to think through this thought. In this case, the control is just returned, with no chance to handle the failed case? Or do you think we follow it with a conditional check?

const value = data?.deep?.down?.here?.optionally ?? return;
if (!value) { handleNegativeCase }

If I am understanding correctly, or could it also be written?

const value = data?.deep?.down?.here ?? { handleNegativeCase return } // implicit return or explicit?

Would this also work in loop blocks?

for(const key in dictionary) {
    const value = dictionary[key]
    const requiredToProceedDetails = value?.requiredDetails ?? return;
}

And then if we can handle or take an action before we return control, based on the assumption above.

for(const key in dictionary) {
    const value = dictionary[key]
    const requiredToProceedDetails = value?.requiredDetails ?? { showErrorMessage return }
}

Honestly, though, this pattern might work today. I did a quick experiment in the console and wasn’t able to get it working the way I imagined in today’s syntax.

const dictionary = { optonalValue: null, populatedValue: "value" }
const errorHandler = () => { console.log("Handled Error") }
const successHandler = () => { console.log("Success") }
function main() {
    const requiredValue = dictionary?.["optonalValue"] ?? (() => { errorHandler(); return null; })()
    console.log("Shouldn't get here: ", requiredValue);
}
// When run prints
main()
Handled Error
Shouldn't get here:  null

It at least returns the value inside the function… anyway. So yeah, let me know if I'm interpreting your suggestion correctly. Thanks for the thoughts. I think it would be cool to see it set up like this. Although I feel like ?? has its own meaning, and is this an overload that would be syntactically appropriate?

1 Like

Thanks for thinking this through — let me try to clarify what I had in mind.

My suggestion was less about introducing a new way to handle the negative case inline, and more about enabling a clean early-exit in expression position. Conceptually, it’s closer to “fail fast and leave the current control flow” rather than “branch and recover”.

So in this mental model, something like:

const value = expr ?? return;

would simply exit the current function (or loop body) immediately, similar to how throw aborts evaluation today. There wouldn’t be an opportunity to continue evaluating the surrounding expression or attach additional logic unless that logic happens before the expression.

Because of that, I wasn’t really imagining block expressions like:

expr ?? { handleNegativeCase return }

That feels like a different (and much larger) design space.

Regarding loops: yes, I was imagining this behaving analogously to return / continue today — i.e. the control-flow effect would be determined by the surrounding context, not by the operator itself.

And to be clear, I’m not necessarily advocating for overloading ?? specifically. It just happens to be a convenient example for “use this value, otherwise exit”. The broader question I was raising is whether some form of expression-level early-exit (like throw) might be a simpler alternative to declaration-level guarded constructs.

Your experiment is interesting — it does show that we can approximate parts of this today, but as you noticed, it doesn’t quite capture the same control-flow semantics.

Thanks for the explanation @juner! Hope you had a good holiday. And yeah, the experiment was interesting for me too. I hadn’t really thought about how I might use that pattern in general, and I don’t think I use ?? enough or under really any advanced use cases. And I think, honestly, it would seem like there is room to do both. I guess, depending on the context, you might want to fail and exit. Thinking of a use case where you make a database call where you fail and exit, but then thinking of the parent of this call, where maybe you want to fail inline if the response was thrown. I would be interested to experiment more with these concepts.

1 Like

Replying to myself since I can’t edit the root post.

I also didn’t find this initially when looking, but it seems someone proposed a similar concept. Try-catch oneliner - #2 by jridgewell . I think I had this in mind initially when proposing mine, but I wanted to avoid introducing new keywords like guard. But I did think about this.

1 Like