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,throwbreak/continuewhere 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
- Should destructuring patterns be allowed?
- Should else require a block or allow a single statement?
- 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.