With the new safe assignment ?=
operator you’ll stop writing code like this:
// ❌ Before:
// ❌ Deep nesting of try-catch for different errors
async function fetchData() {
try {
const response = await fetch('https://api.codingbeautydev.com/docs');
try {
const data = await response.json();
return data;
} catch (parseError) {
console.error(parseError);
}
} catch (networkError) {
console.error(networkError);
}
}
And start writing code like this:
// ✅ After:
async function fetchData() {
const [networkError, response] ?= await fetch('https://codingbeautydev.com');
if (networkError) return console.error(networkError);
const [parseError, data] ?= await response.json();
if (parseError) return console.error(parseError);
return data;
}
We’ve completely eradicated the deep nesting. The code is far more readable and cleaner.
Instead of getting the error in the clunky catch
block:
async function doStuff() {
try {
const data = await func('codingbeautydev.com');
} catch (error) {
console.error(error);
}
}
Now we do everything in just one line.
Instead of failing loudly and proudly, ?=
tells the error to shut up and let us decide what do with it.
// ✅ if there's error: `err` has value, `data` is null
// ✅ no error: `err` is null, `data has value
async function doStuff() {
const [err, data] ?= await func('codingbeautydev.com');
}
We can tell it to get lost:
async function doStuff() {
// 👇 it's as good as gone here
const [, data] ?= await func('codingbeautydev.com');
// ...
}
We can announce it to the world and keep things moving:
async function doStuff() {
const [err, data] ?= await func('codingbeautydev.com');
if (err) {
console.error(err);
}
// ...
}
Or we can stop immediately:
// `err` is null if there's no error
async function doStuff() {
const [err, data] ?= await func('codingbeautydev.com');
if (err) return;
}
Which makes it such a powerful tool for creating guard clauses:
// ✅ avoid nested try-catch
// ✅ avoid nested ifs
function processFile() {
const filename = 'codingbeautydev.com.txt';
const [err, jsonStr] ?= fs.readFileSync(filename, 'utf-8');
if (readErr) {
return;
}
const [jsonErr, json] ?= JSON.parse(jsonStr);
if (jsonErr) {
return;
}
const awards = json.awards.length;
console.log(`🏅Total awards: ${awards}`);
}
And here’s one of the very best things about this new operator.
There are several instances where we want a value that depends on whether or not there’s an exception.
Normally you’ll use a mutable var outside the scope for error-free access:
function writeTransactionsToFile(transactions) {
// ❌ mutable var
let writeStatus;
try {
fs.writeFileSync(
'codingbeautydev.com.txt',
transactions
);
writeStatus = 'success';
} catch (error) {
writeStatus = 'error';
}
// do something with writeStatus...
}
But this can be frustrating, especially when you’re trying to have immutable code and the var was already const
before the time came to add try-catch.
You’d have to wrap it try
, then remove the const
, then make a let
declaration outside the try
, then re-assign again in the catch
…
But now with ?=
:
function writeTransactionsToFile(transactions) {
const [err, data] ?= fs.writeFileSync(
'codingbeautydev.com.txt',
transactions
)
const writeStatus = err ? 'error' : 'success'
// // do something with writeStatus...
}
We maintain our immutability and the code is now much more intuitive. Once again we’ve eradicated all nesting.
How does it work?
The new ?=
operator calls the Symbol.result
method internally.
So when we do this:
const [err, result] ?= func('codingbeautydev.com');
This is what’s actually happening:
// it's an assignment now
const [err, result] = func[Symbol.result](
'codingbeautydev.com'
);
So you know what this means right?
It means we can make this work with ANY object that implements Symbol.result
:
function doStuff() {
return {
[Symbol.result]() {
return [new Error("Nope"), null];
},
};
}
const [error, result] ?= doStuff();
But of course you can throw as always:
function doStuff() {
throw new Error('Nope');
}
const [error, result] ?= doStuff();
And one cool thing it does: if result
has its own Symbol.result
method, then ?=
drills down recursively:
function doStuff() {
return {
[Symbol.result](str) {
console.log(str);
return [
null,
{
[Symbol.result]() {
return [new Error('WTH happened?'), null];
}
}
];
}
}
}
const [error, result] ?= doStuff('codingbeautydev.com');
You can also use the object directly instead of returning from a function:
const obj = {
[Symbol.result]() {
return [new Error('Nope'), null];
},
};
const [error, result] ?= obj;
Although, where would this make any sense in practice?
As we saw earlier, ?=
is versatile enough to fit in seamlessly with both normal and await
ed functions.
const [error, data] ?= fs.readFileSync('file.txt', 'utf8');
const [error, data] ?= await fs.readFile('file.txt', 'utf8');
using
?=
also works with the using
keyboard to automatically clean up resources after use.
❌ Before:
try {
using resource = getResource();
} catch (err) {
// ...
}
✅ After:
using [err, resource] ?= getResource();
How to use it now
While we wait for ?=
to become natively integrated into JavaScript, we can start it now with this polyfill:
But you can’t use it directly — you’ll need Symbol.result
:
import 'polyfill.js';
const [error, data] = fs
.readFileSync('codingbeautydev.com.txt', 'utf-8')
[Symbol.result]();
Final thoughts
JavaScript error handling just got much more readable and intuitive with the new safe assignment operator (?=)
.
Use it to write cleaner and more predictable code.
11 Amazing New JavaScript Features in ES13
This guide will bring you up to speed with all the latest features added in ECMAScript 13. These powerful new features will modernize your JavaScript with shorter and more expressive code.