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.
Every Crazy Thing JavaScript Does
A captivating guide to the subtle caveats and lesser-known parts of JavaScript.