DEV Community

Daniel Keya
Daniel Keya

Posted on

JavaScript's Safe Assignment Operator (?=): Hype vs. Reality

If you've scrolled through dev.to lately, you've probably seen headlines like "Say goodbye to try/catch!" promoting a new JavaScript operator: ?=, the safe assignment operator.

It's a neat idea, and worth understanding. But before you rewrite your codebase, there's an important caveat: you can't use it, and it's unlikely you ever will. Let's look at what it proposes, why people liked it, and what you can do today to get the same benefits.

The problem: try/catch gets noisy

Error handling around async code often looks like this:

async function loadUser(id) {
  let response;
  try {
    response = await fetch(`/api/users/${id}`);
  } catch (error) {
    console.error("Network error", error);
    return null;
  }

  let user;
  try {
    user = await response.json();
  } catch (error) {
    console.error("Invalid JSON", error);
    return null;
  }

  return user;
}
Enter fullscreen mode Exit fullscreen mode

Every fallible step needs its own try block. Variables have to be declared with let outside the block so they're visible afterward. The happy path gets buried.

The proposal: errors as values

The safe assignment operator proposal suggests a new operator, ?=, that runs the expression on the right and gives you back a tuple of [error, result]:

async function loadUser(id) {
  const [networkError, response] ?= await fetch(`/api/users/${id}`);
  if (networkError) {
    console.error("Network error", networkError);
    return null;
  }

  const [parseError, user] ?= await response.json();
  if (parseError) {
    console.error("Invalid JSON", parseError);
    return null;
  }

  return user;
}
Enter fullscreen mode Exit fullscreen mode

The rules are simple:

  • If the call succeeds, you get [null, result].
  • If it throws (or the promise rejects), you get [error, null].

If you've written Go (val, err := doThing()) or used Rust's Result, this will feel familiar. Putting the error first in the tuple nudges you to check it before touching the data.

The proposal also described a Symbol.result protocol, so objects could define how they report success and failure through that tuple shape.

The reality check: it's not happening (at least not as ?=)

Here's the part many tutorials skip. The proposal started as a draft from a community member. It got a lot of attention on social media and then a lot of pushback on the ECMAScript discussion boards. Critics argued that:

  • It isn't really about assignment. The operator acts on the expression on the right, but errors can be thrown in other places before that call even happens.
  • ?= looks too much like existing operators, like the nullish coalescing assignment ??=, and is easy to misread.
  • It only works in assignments and declarations, while a try-expression approach can be used anywhere an expression can.

The proposal's own repository says it will move toward try-expressions as a more idiomatic way to solve the same problem. Several commenters on dev.to posts about ?= point out that the operator was effectively abandoned. So treat any article telling you this is "coming soon" with skepticism.

Syntax like this will not run in Node, Deno, Bun, or any browser:

const [error, data] ?= await fetch(url); // SyntaxError today
Enter fullscreen mode Exit fullscreen mode

What you can do today

The good news is that the idea doesn't need new syntax. A tiny helper gives you the same ergonomics:

async function safe(promiseOrFn) {
  try {
    const value = await (
      typeof promiseOrFn === "function" ? promiseOrFn() : promiseOrFn
    );
    return [null, value];
  } catch (error) {
    return [error, null];
  }
}

const [error, response] = await safe(fetch("/api/data"));
if (error) {
  // handle it
}
Enter fullscreen mode Exit fullscreen mode

And a synchronous version:

function safeSync(fn) {
  try {
    return [null, fn()];
  } catch (error) {
    return [error, null];
  }
}

const [parseError, config] = safeSync(() => JSON.parse(rawText));
Enter fullscreen mode Exit fullscreen mode

There are also small libraries on npm that do the same thing, if you'd rather not maintain your own helper.

A note on TypeScript

You can make this type-safe with a discriminated tuple:

type Result<T, E = unknown> = [error: null, value: T] | [error: E, value: null];

async function safe<T>(promise: Promise<T>): Promise<Result<T>> {
  try {
    return [null, await promise];
  } catch (error) {
    return [error, null];
  }
}

const [error, user] = await safe(getUser(1));
if (error) return;
user; // narrowed to User
Enter fullscreen mode Exit fullscreen mode

Should you adopt this pattern?

It's worth being honest about the trade-offs.

Pros

  • Flatter, more linear code
  • Errors are handled right next to the call that caused them
  • No let declarations hoisted out of try blocks

Cons

  • You can silently ignore the error if you forget the if (error) check
  • It's not idiomatic JavaScript, so teammates may find it unfamiliar
  • Throwing and catching still works better when you want errors to bubble up to a central handler

A good rule of thumb: use the tuple style for local, expected failures (a network call you want to retry, parsing user input), and keep try/catch for unexpected failures that should propagate.

Wrapping up

The safe assignment operator is a good case study in how JavaScript evolves. A catchy idea spreads fast, the community debates it, and the final shape often differs from the first sketch. ?= itself probably won't ship, but the underlying question of how to make error handling less verbose is very much alive, and try-expressions are where that conversation seems to be heading.

In the meantime, a ten-line helper function gets you most of the benefit today.

What do you think: would you use errors-as-values in your JavaScript, or do you prefer good old try/catch? Let me know in the comments.

Top comments (0)