javascript

5 amazing new JavaScript features in ES15 (2024)

2024: Another incredible year of brand new JS feature upgrades with ES15.

From sophisticated async features to syntactic array sugar and modern regex, JavaScript coding is now easier and faster than ever.

1. Native array group-by is here

Object.groupBy():

JavaScript
const fruits = [ { name: 'pineapple🍍', color: '🟡' }, { name: 'apple🍎', color: '🔴' }, { name: 'banana🍌', color: '🟡' }, { name: 'strawberry🍓', color: '🔴' }, ]; const groupedByColor = Object.groupBy( fruits, (fruit, index) => fruit.color ); console.log(groupedByColor);

Literally the only thing keeping dinosaur Lodash alive — no more!

I was expecting a new instance method like Array.prototype.groupBy but they made it static for whatever reason.

Then we have Map.groupBy to group with object keys:

JavaScript
const array = [1, 2, 3, 4, 5]; const odd = { odd: true }; const even = { even: true }; Map.groupBy(array, (num, index) => { return num % 2 === 0 ? even: odd; }); // => Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }

Almost no one ever groups arrays this way though, so will probably be far less popular.

2. Resolve promise from outside — modern way

With Promise.withResolvers().

It’s very common to externally resolve promises and before we had to do it with a Deferred class:

JavaScript
class Deferred { constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } } const deferred = new Deferred(); deferred.resolve();

Or install from NPM — one more dependency.

But now with Promise.withResolvers():

JavaScript
const { promise, resolve, reject } = Promise.withResolvers();

See how I use it to rapidly promisify an event stream — awaiting an observable:

JavaScript
// data-fetcher.js // ... const { promise, resolve, reject } = Promise.withResolvers(); export function startListening() { eventStream.on('data', (data) => { resolve(data); }); } export async function getData() { return await promise; } // client.js import { startListening, getData } from './data-fetcher.js'; startListening(); // ✅ listen for single stream event const data = await getData();

3. Buffer performance upgrades

Buffers are tiny data stores to store temporary data your app generates.

They make it incredible easy to transfer and process data across various stages in a pipeline.

Pipelines like:

  • File processing: Input file buffer process new buffer output file
  • Video streaming: Network response buffer display video frame
  • Restaurant queues: Receive customer queue/buffer serve customer
JavaScript
const fs = require('fs'); const { Transform } = require('stream'); const inputFile = 'input.txt'; const outputFile = 'output.txt'; const inputStream = fs.createReadStream(inputFile, 'utf-8'); const transformStream = new Transform({ transform(chunk) { // ✅ tranform chunks from buffer }, }); const outputStream = fs.createWriteStream(outputFile); // ✅ start pipeline inputStream.pipe(transformStream).pipe(outputStream);

With buffers, each stage process data at different speeds independent of each other.

But what happens when the data moving through the pipeline exceeds the buffers capacity?

Before we’d have to copy all the current data’s buffer to a bigger buffer.

Terrible for performance, especially when there’s gonna be a LOT of data in the pipeline.

ES15 gives us a solution to this problem: Resizable array buffers.

JavaScript
const resizableBuffer = new ArrayBuffer(1024, { maxByteLength: 1024 ** 2, }); // ✅ resize to 2048 bytes resizableBuffer.resize(1024 * 2);

4. Asynchronous upgrades

Atomics.waitAsync(): Another powerful async coding feature in ES2024:

It’s when 2 agents share a buffer…

And agent 1 “sleeps” and waits for agent 2 to complete a task.

When agent 2 is done, it notifies using the shared buffer as a channel.

JavaScript
const sharedBuffer = new SharedArrayBuffer(4096); const bufferLocation = new Int32Array(sharedBuffer); // ✅ initial value at buffer location bufferLocation[37] = 0x1330; async function doStuff() { // ✅ agent 1: wait on shared buffer location until notify Atomics.waitAsync(bufferLocation, 37, 0x1330).then((r) => {} /* handle arrival */); } function asyncTask() { // ✅ agent 2: notify on shared buffer location const bufferLocation = new Int32Array(sharedBuffer); Atomics.notify(bufferLocation, 37); }

You’d be absolutely right if you thought this similar to normal async/await.

But the biggest difference: The 2 agents can exist in completely different code contexts — they only need access to the same buffer.

And: multiple agents can access or wait on the shared buffer at different times — and any one of them can notify to “wake up” all the others.

It’s like a P2P network. async/await is like client-server request-response.

JavaScript
const sharedBuffer = new SharedArrayBuffer(4096); const bufferLocation = new Int32Array(sharedBuffer); bufferLocation[37] = 0x1330; // ✅ received shared buffer from postMessage() const code = ` var ia = null; onmessage = function (ev) { if (!ia) { postMessage("Aux worker is running"); ia = new Int32Array(ev.data); } postMessage("Aux worker is sleeping for a little bit"); setTimeout(function () { postMessage("Aux worker is waking"); Atomics.notify(ia, 37); }, 1000); }`; async function doStuff() { // ✅ agent 1: exists in a Worker context const worker = new Worker( 'data:application/javascript,' + encodeURIComponent(code) ); worker.onmessage = (event) => { /* log event */ }; worker.postMessage(sharedBuffer); Atomics.waitAsync(bufferLocation, 37, 0x1330).then( (r) => {} /* handle arrival */ ); } function asyncTask() { // ✅ agent 2: notify on shared buffer location const bufferLocation = new Int32Array(sharedBuffer); Atomics.notify(bufferLocation, 37); }

5. Regex v flag & set operations

A new feature to make regex patters much more intuitive.

Finding and manipulating complex strings using expressive patterns — with the help of set operations:

JavaScript
// A and B are character class, like [a-z] // difference: matches A but not B [A--B] // intersection: matches both A & b [A&&B] // nested character class [A--[0-9]]

To match ever-increasing sets of Unicode characters, like:

  • Emojis: 😀, ❤️, 👍, 🎉, etc.
  • Accented letters: é, à, ö, ñ, etc.
  • Symbols and non-Latin characters: ©, ®, €, £, µ, ¥, etc

So here we use Unicode regex and the v flag to match all Greek letters:

JavaScript
const regex = /[\p{Script_Extensions=Greek}&&\p{Letter}]/v;

Final thoughts

Overall ES15 is significant leap for JavaScript with several features essential for modern development.

Empowering you to write cleaner code with greater conciseness, expressiveness, and clarity.

structuredClone(): The easiest way to deep copy objects in JavaScript

Deep copying is a regular programming task for passing or storing data.

  • Shallow copy: Only copies the first level of the object
  • Deep copy: Copies all levels of the object
JavaScript
const obj = { name: 'Tari', friends: [{ name: 'Messi'}]}; const shallowCopy = { ...obj }; const deepCopy = dCopy(obj); console.log(obj.friends === shallowCopy.friends); // ❌ true console.log(obj.friends === deepCopy.friends); // ✅ false

But all this while we’ve never had a built-in way to perfectly deep copy objects and it’s been a pain.

We always had to lean on third-party libraries for deep copying and keeping circular references.

All that changes now with the new structuredClone() — an easy and efficient way to deep copy any object.

JavaScript
const obj = { name: 'Tari', friends: [{ name: 'Messi' }] }; const clonedObj = structuredClone(obj); console.log(obj.name === clonedObj); // false console.log(obj.friends === clonedObj.friends); // false

Cloning circular references with ease:

JavaScript
const car = { make: 'Toyota', }; // 👉 Circular reference car.basedOn = car; const cloned = structuredClone(car); console.log(car.basedOn === cloned.basedOn); // false // 👇 Circular reference is cloned console.log(car === car.basedOn); // true

Something you could never do with the JSON stringify/parse hack:

Go as deep as you want:

JavaScript
// ↘️ const obj = { a: { b: { c: { d: { e: 'Coding Beauty', }, }, }, }, }; const clone = structuredClone(obj); console.log(clone.a.b.c.d === obj.a.b.c.d); // ✅ false console.log(clone.a.b.c.d.e); // Coding Beauty

Limitations you should know

structuredClone() is very powerful but it has important weaknesses you should know:

Can’t clone functions or methods

Because of the special algorithm it uses.

Can’t clone DOM elements

Doesn’t preserve RegExp lastIndex property

I mean, no one’s cloning regexes but it’s one to note:

JavaScript
const regex = /beauty/g; const str = 'Coding Beauty: JS problems are solved at Coding Beauty'; console.log(regex.index); console.log(regex.lastIndex); // 7 const regexClone = structuredClone(regex); console.log(regexClone.lastIndex); // 0

Other limitations

Important to be aware of them to avoid unexpected behavior when using the function.

Clone some, move

This is a more sophisticated one.

You transfer inner objects from source to clone instead of copying.

Which means there’s nothing left in source to change:

JavaScript
const uInt8Array = Uint8Array.from({ length: 1024 * 1024 * 16 }, (v, i) => i); const transferred = structuredClone(uInt8Array, { transfer: [uInt8Array.buffer], }); console.log(uInt8Array.byteLength); // 0

Overall structuredClone() is a valuable addition to a developer’s toolkit and makes object cloning easier than ever in JavaScript

Recreate the Material Design text field with HTML, CSS, and JavaScript

No doubt you’ve seen the beautiful text field if you’re one of Gmail’s 2 billion active users:

It’s fluid, it’s intuitive, it’s colorful 🎨.

It’s Material Design: the wildly popular UI design system powering YouTube, WhatsApp, and many other apps with billions of users.

Let’s embark on a journey of recreating it from scratch with pure vanilla HTML, CSS, and JavaScript.

1. Start: Create basic input and label

As always we start with the critical HTML foundation, the skeleton:

The text input, a label, and a wrapper for both:

HTML
<!-- For text animation -- soon --> <div class="input-container"> <input type="text" id="fname" name="fname" value="" aria-labelledby="label-fname" /> <label class="label" for="fname" id="label-fname"> <div class="text">First Name</div> </label> </div>

2. Style input and label

I find it pretty satisfying: using CSS to gradually flesh out a stunning UI on the backs of a solid HTML foundation.

Let’s start:

Firs the <input> and its container:

CSS
.input-container { position: relative; /* parent of .label */ } input { height: 48px; width: 280px; border: 1px solid #c0c0c0; border-radius: 4px; box-sizing: border-box; padding: 16px; } .label { /* to stack on input */ position: absolute; top: 0; bottom: 0; left: 16px; /* match input padding */ /* center in .input-container */ display: flex; align-items: center; } .label .text { position: absolute; width: max-content; }

3. Remove pointer events

It resembles a text field now, but look what happens when I try focusing:

The label is part of the text field and the cursor should reflect that:

Solution? cursor: text

CSS
.label { ... cursor: text; /* Prevent blocking <input> focus */ pointer-events: none; }

4. Style input font

Now it’s time to customize font settings:

If you know Material Design well, you know Roboto is at the center of everything — much to the annoyance of some.

We’ll grab the embed code from Google Fonts:

Embed:

Use:

CSS
input, .label .text { font-family: 'Roboto'; font-size: 16px; }

5. Style input on focus

You’ll do this with the :focus selector:

CSS

input:focus {
  outline: none;
  border: 2px solid blue;
}

6. Fluidity magic: Style label on input focus

On focus the label does 3 things:

  1. Shrinks
  2. Move to top input border
  3. Match input border color

Of course we can do all these with CSS:

CSS
input:focus + .label .text { /* 1. Shrinks */ font-size: 12px; /* 2. Move to top input border */ transform: translate(0, -100%); top: 15%; padding-left: 4px; padding-right: 4px; /* 3. Match input border color */ background-color: white; color: #0b57d0; }

All we need to complete the fluidity is CSS transition:

CSS
label .text { transition: all 0.15s ease-out; }

7. One more thing

Small issue: The label always goes to the original position after the input loses focus:

Because it depends on CSS :focus which goes away on focus lost.

But this should only happen when there’s no input yet.

CSS can’t fix this alone, we’re going to deploy the entire 3-tiered army of web dev.

HTML: input value to zero.

HTML
<input type="text" id="fname" name="fname" value="" aria-labelledby="label-fname" />

CSS: :not selector to give unfocused input label same position and size when not empty:

JavaScript
input:focus + .label .text, /* ✅ no input yet */ :not(input[value='']) + .label .text { /* 1. Shrink */ font-size: 12px; transform: translate(0, -100%); /* 2. Move to top */ top: 15%; padding-left: 4px; padding-right: 4px; /* 3. Active color */ background-color: white; color: #0b57d0; }

And JavaScript: Sync initial input value attribute with user input

JavaScript
const input = document.getElementById('fname'); input.addEventListener('input', () => { input.setAttribute('value', input.value); });
JavaScript
const input = document.getElementById('fname'); input.addEventListener('input', () => { input.setAttribute('value', input.value); });

That’s it! We’ve successfully created an outlined Material Design text field.

With React or Vue it’ll be pretty easy to abstract everything we’ve done into a reusable component.

Here’s the link to the full demo: CodePen

Why [‘1’, ‘5, ’11’].map(parseInt) returns [1, NaN, 3] in JavaScript

Alex, the self-proclaimed coding whiz, prided himself on quick hacks and short code.

Despite being very new to the industry, he always saw himself as superior to the rest of the team, stubbornly doing whatever he felt like; all their well-meant advice falling on deaf ears.

But Alex was soon to meet his catastrophic downfall. A painful, humbling experience he would never forget.

It all started when Alex and Cody were assigned a project task. To let users view the products for the ongoing eCommerce website the team had been working on.

They were still in startup phase so all the data was stored and updated in a CSV file.

The product name, price, quantity… all the usual stuff you’d see on sites like Amazon.

Alex arrogantly scoffed when he learned of the planned collaboration.

“I don’t need to work with ANYONE okay?” He smirked at Jake, the good-natured head of engineering, as he typed on his PC. “It’s literally just to fetch from the DB and show in the JSX”.

“Alex you need to learn how to work with others, I keep telling you this.”, Jake responded with a patient, forced smile. He was used to this man’s self-obsessed antics.

“I don’t need to work with anyone, I can do this alone. Cody will only slow me down with all his nonsense talk about readable code”.

“Cody is one of our best and takes his time for a reason. I keep telling you it’s not all about writing code quickly or concisely…”.

“You always keep telling me things but you NEVER listen to me. I just want to work on my own this time, okay?”. “Please?”, Alex quickly added to avoid sounding too rude — keeping that snobbish smirk on his face of course.

Jake sighed.

“Alright, I’ll let you work you alone if you can convert this string array”, as he wrote on a nearby sheet of paper, “to the same array of numbers”.

Alex couldn’t believe it. On the paper was a simple array.

JavaScript
['1', '5', '11']

This had to be a trick question. He looked up at Jake suspiciously.

“Seriously? How dumb do you think I am that I can’t parse this?”

“Do it, you only get one chance”. Jake deserved a self-control medal for his astounding patience with this youngster.

With a smug look on his face, Alex opened up a new VS Code terminal and smugly typed out the seemingly obvious solution in Node:

JavaScript
['1', '5', '11'].map(parseInt)

He smirked triumphantly, only to turn and see a knowing smile on Jake’s face — he was thrown off balance instantly.

“You sure about that Alex? Why don’t you press Enter and let’s see what the resulting array actually is”.

A little unsure of himself, he scanned through the short CLI code to ensure absolute certainty, before the final moment.

What he saw next shook him to his very core.

How on earth was this possible? Was parseInt broken? Was there a bug in map()?

He looked up frantically, eliciting a sharp, unnerving laughter from Jake.

“Alex, you’re fired”.

“WHAT?!”, Alex screamed.

“Pack your things and get out of here before I close and open my eyes, you arrogant buffoon!”

You see, Alex’s downfall was NOT his failure to understand map and parseInt — though that could have helped.

It was his obsession with making code as short as possible, at the expense of readability and clarity…

The fact is in 99% of cases this is how we use map and parseInt

JavaScript
const doubles = ['1', '5', '11'].map((num) => num * 2); console.log(doubles); // [2, 10, 22] const num = parseInt('5'); console.log(num); // 👍 5 -- not NaN!

But you may be shocked at what happens when you use map with console.log:

JavaScript
const doubles = ['1', '2', '3'].map(console.log);

It logs 3 pairs of numbers for each item!

That’s because the map() callback actually takes 3 arguments:

So you’re actually calling parseInt with 3 args:

JavaScript
// parseInt take 2 args max but JS compiler doesn't complain ['1', '5', '11'].map(parseInt) // parseInt('1', '0', ['1', '5', '11']) // parseInt('5', '1', ['1', '5', '11']) // parseInt('11' '2', ['1', '5', '11'])

Alex never knew that parseInt takes either 1 or 2 arguments and behaves differently for each:

When there’s a 2nd arg it becomes a base for the 1st number arg:

JavaScript
// 👇 invalid positive number, ignore parseInt('1', '0'); // 1 ✅ parseInt('3', 'blah blah'); // 3 // 👇 invalid base (1) parseInt('2', '1'); // NaN parseInt('5', '1'); // NaN ✅ // 👇 '10' is 2 in base 2 (remember?) parseInt('10', '2'); // 2 parseInt('11', '2'); // 3 ✅

Despite his average knowledge of map and parseInt, he could have avoided all of these by simply being explicit:

JavaScript
['1', '5', '11'].map((num) => parseInt(num));

Shortening code can be great for reducing clutter but we should always prioritize clear and readable code:

Especially when the added length is not that big of a deal, you know?

JavaScript
async function setTimeout() { // ❌ await new Promise((resolve) => setTimeout(resolve, 1000)); console.log('Coding Beauty'); } async function setTimeout() { // // ✅ await new Promise((resolve) => setTimeout(() => resolve(), 1000)); console.log('Coding Beauty'); }

Resolve a promise from outside in JavaScript: practical use cases

It’s one of those “cool” things you can do in JavaScript that are actually immensely powerful when put to good use.

JavaScript
let promiseResolve; let promiseReject; const promise = new Promise((resolve, reject) => { promiseResolve = resolve; promiseReject = reject; }); promiseResolve();

Powerful practical use cases

Action B waiting for action A

A is ongoing but the user wants to do B but A needs to happen first.

Example: Social app where users can create, save, and publish posts. Like Medium.

HTML
<p> Save status: <b><span id="save-status">Not saved</span></b> </p> <p> Publish status: <b><span id="publish-status">Not published</span></b> </p> <button id="save">Save</button> <button id="publish">Publish</button>
A simple app where users can create, save and publish posts.
Publish doesn’t happen until after save.

What if the user wants to publish the post when it’s saving?

Solution: Ensure the post is saved before publishing happens.

JavaScript
saveButton.onclick = () => { save(); }; publishButton.onclick = async () => { await publish(); }; let saveResolve; let hasSaved = false; async function save() { hasSaved = false; saveStatus.textContent = 'Saving...'; // ✅ Resolve promise from outside await makeSaveRequest(); saveResolve(); hasSaved = true; saveStatus.textContent = 'Saved'; } async function waitForSave() { if (!hasSaved) { await new Promise((resolve) => { saveResolve = resolve; }); } } async function publish() { publishStatus.textContent = 'Waiting for save...'; await waitForSave(); publishStatus.textContent = 'Published'; return; }
Post is saved before publish happens.

It gets even better when you abstract this logic into a kind of Deferred class:

JavaScript
class Deferred { constructor() { this.promise = new Promise((resolve, reject) => { this.reject = reject; this.resolve = resolve; }); } } const deferred = new Deferred(); // Resolve from outside deferred.resolve();

Refactoring✅:

JavaScript
// ... const deferredSave = new Deferred(); let hasSaved = false; async function save() { hasSaved = false; saveStatus.textContent = 'Saving...'; // ✅ Resolve promise from outside await makeSaveRequest(); saveResolve(); hasSaved = true; saveStatus.textContent = 'Saved'; } async function waitForSave() { if (!hasSaved) await deferredSave.promise; } async function publish() { // ... }

And it works exactly like before:

The functionality works as before after the refactor.

Deferred is much cleaner, which is why we’ve got tons of NPM libraries like it: ts-deferred, deferred, promise-deferred

Promisifying an event stream

It’s a great setup I’ve used multiple times.

Doing an async task that’s actually waiting for an event stream to fire, internally:

JavaScript
// data-fetcher.js const deferred = new Deferred(); let dataDeferred; function startListening() { dataDeferred = new Deferred(); eventStream.on('data', (data) => { dataDeferred.resolve(data); }); } async function getData() { return await dataDeferred.promise; } class Deferred { constructor() { this.promise = new Promise((resolve, reject) => { this.reject = reject; this.resolve = resolve; }); } } // client.js const { startListening, getData } = require('./data-fetcher.js'); startListening(); const data = await getData();

Final thoughts

Resolving promises externally unlocks powerful patterns.

From user actions to event streams, it keeps your code clean and flexible. Consider libraries like ts-deferred for even better handling.

The 5 most transformative JavaScript features from ES9

JavaScript has come a long way in the past 10 years with brand new feature upgrades in each one.

Let’s look at the 5 most significant features that arrived in ES9; and see the ones you missed.

1. Async generation and iteration

Async generators was a powerful one from ES9.

Just like normal generators but now it pops out the values after asynchronous work like a network request or something:

JavaScript
function* asyncGenerator() { yield new Promise((resolve) => setTimeout(() => resolve('done this ✅'), 2000) ); yield new Promise((resolve) => setTimeout(() => resolve('done that ✅'), 3000) ); }

So when we call .next() we’ll get a Promise:

JavaScript
const asyncGen = asyncGenerator(); asyncGen.next().value.then(console.log); asyncGen.next().value.then(console.log);

It’s such a powerful tool for streaming data in web app in a structured + readable manner — just look at this function that buffers and streams data for a video-sharing app like YouTube:

JavaScript
async function* streamVideo({ id }) { let endOfVideo = false; const downloadChunk = async (sizeInBytes) => { const response = await fetch( `api.example.com/videos/${id}` ); const { chunk, done } = await response.json(); if (done) endOfVideo = true; return chunk; }; while (!endOfVideo) { const bufferSize = 500 * 1024 * 1024; yield await downloadChunk(bufferSize); } }

And now to consume this generator we’ll use for await of — async iteration:

JavaScript
for await (const chunk of streamVideo({ id: 2341 })) { // process video chunk }

I wonder if the actual YouTube JavaScript code uses generators like this?

2. Rest / spread operator

No doubt you’ve stumbled upon the modern spread syntax somewhere.

A genius way to rapidly and immutable clone arrays:

JavaScript
const colors = ['🔴', '🔵', '🟡']; console.log([...colors, '🟢']); // [ '🔴', '🔵', '🟡', '🟢' ]

We never had it before ES9, and now it’s all over the place.

Redux is big one:

JavaScript
export default function userState(state = initialUserState, action){ console.log(arr); switch (action.type){ case ADD_ITEM : return { ...state, arr:[...state.arr, action.newItem] } default:return state } }

And it works for objects too:

JavaScript
const info = { name: 'Coding Beauty', site: 'codingbeautydev.com', }; console.log({ ...info, theme: '🔵' }); // { name: 'Coding Beauty', // site: 'codingbeautydev.com', // theme: '🔵' }

Overrides props:

JavaScript
const langs = { j: 'java', c: 'c++', }; console.log({ ...langs, j: 'javascript ' }); // { j: 'javascript ', c: 'c++' }

This makes it especially great for building upon default values, especially when making a public utility.

Or customizing a default theme like I did with Material UI:

With the spread syntax you can even scoop out an object’s copy without properties you don’t want.

JavaScript
const colors = {   yellow: '🟡',   blue: '🔵',   red: '🔴', }; const { yellow, ...withoutYellow } = colors; console.log(withoutYellow); // { blue: '🔵', red: '🔴' }

That’s how you remove properties from an object immutably.

3. String.raw

When I use String.raw I’m saying: Just give me what I give you. Don’t process anything.

Leave those escape characters alone:

JavaScript
// For some weird reason you can use it without brackets // like this 👇 const message = String.raw`\n is for newline and \t is for tab`; console.log(message); // \n is for newline and \t is for tab

No more escaping backslashes:

JavaScript
const filePath = 'C:\\Code\\JavaScript\\tests\\index.js'; console.log(`The file path is ${filePath}`); // The file path is C:\Code\JavaScript\tests\index.js

We can write:

JavaScript
const filePath = String.raw`C:\Code\JavaScript\tests\index.js`; console.log(`The file path is ${filePath}`); // The file path is C:\Code\JavaScript\tests\index.js

Perfect for writing regexes with a stupid amount of these backslashes:

Something like this but much worse:

From this✅:

JavaScript
const patternString = 'The (\\w+) is (\\d+)'; const pattern = new RegExp(patternString); const message = 'The number is 100'; console.log(pattern.exec(message)); // ['The number is 100', 'number', '100']

To this✅:

JavaScript
const patternString = String.raw`The (\w+) is (\d+)`; const pattern = new RegExp(patternString); const message = 'The number is 100'; console.log(pattern.exec(message)); // ['The number is 100', 'number', '100']

So “raw” as in unprocessed.

That’s why we have String.raw() but no String.cooked().

4. Sophisticated regex features

And speaking of regexes ES9 didn’t disappoint.

It came fully loaded with state-of-the-art regex features for advanced string searching and replacing.

Look-behind assertions

This was a new feature to make sure that only a certain pattern comes before what you’re searching for:

  • Positive look-behind: Whitelist ?<=pattern
  • Negative look-behind: Blacklist ?<!pattern
JavaScript
const str = "It's just $5, and I have €20 and £50"; // Only match number sequence if $ comes first const regexPos = /(?<=\$)\d+/g; console.log(str.match(regexPos)); // ['5'] const regexNeg = /(?<!\$)(\d+)/g; // ['20', '50' ] console.log(str.match(regexNeg));

Named capture groups

Capture groups has always been one of the most invaluable regex features for transforming strings in complex ways.

JavaScript
const str = 'The cat sat on a map'; // $1 -> [a-z] // $2 -> a // $3 -> t // () indicates group str.replace(/([a-z])(a)(t)/g, '$1*$3'); // -> The c*t s*t on a map

So normally the groups go by their relative position in the regex: 1, 2, 3…

But this made understanding and changing those stupidly long regexes much harder.

So ES9 solved this with ?<name> to name capture groups:

JavaScript
const str = 'The cat sat on a map'; // left & right console.log(str.replace(/(?<left>[a-z])(a)(?<right>t)/g, '$<left>*$<right>')); // -> The c*t s*t on a map

You know how when things break in VS Code, you can quickly Alt + Click to go to the exact point where it happened? 👇

VS Code uses capture groups to make the filenames clickable and make this rapid navigation possible.

I’d say it’s something like this:

JavaScript
// The stupidly long regex const regex = /(?<path>[a-z]:(?:(?:\/|(?:\\?))[\w \.-]+)+):(?<line>\d+):(?<char>\d+)/gi; // ✅ String.raw! const filePoint = String.raw`C:\coding-beauty\coding-beauty-javascript\index.js:3:5`; const extractor = /(?<path>[a-z]:(?:(?:\/|(?:\\?))[\w \.-]+)+):(?<line>\d+):(?<char>\d+)/i; const [path, lineStr, charStr] = filePoint .match(regex)[0] .match(extractor) .slice(1, 4); const line = Number(lineStr); const char = Number(charStr); console.log({ path, line, char }); // Replace all filePoint with <button> tag // <button onclick="navigateWithButtonFilepointInnerText">{filePoint}</button>

5. Promise.finally

Finally we have Promise.finally 😉.

You know how finally always run some code whether errors are there or not?

JavaScript
function startBodyBuilding() { if (Math.random() > 0.5) { throw new Error("I'm tired😫"); } console.log('Off to the gym 👟💪'); } try { startBodyBuilding(); } catch { console.log('Stopped excuse🛑'); } finally { console.log("I'm going!🏃"); }

So Promise.finally is just like that but for async tasks:

JavaScript
async function startBodyBuilding() { await think(); if (Math.random() > 0.5) { throw new Error("I'm tired😫"); } console.log('Off to the gym 👟💪'); } startBodyBuilding() .then(() => { console.log('Started ✅'); }) .catch(() => { console.log('No excuses'); }) .finally(() => { console.log("I'm going!🏃"); });

The biggest pro of Promise.finally() is when you’re chaining lots of Promises:

It also works well with Promise chains:

JavaScript
getFruitApiUrl().then((url) => { return fetch(url) .then((res) => res.json()) .then((data) => { fruits.push(data); }) .catch((err) => { console.error(err); }) .finally(() => { console.log(fruits); }); });

Brought forth by ES9.

Final thoughts

ES9 marked a significant leap forward for JavaScript with several features that have become essential for modern development.

Empowering you to write cleaner code with greater conciseness, expressiveness, and clarity.

You don’t actually need if statements (EVER)

Sure they’re a nice and easy way to create control flow, but you can write many billions of lines of conditional JS code without a SINGLE if statement.

And there are many situations where a different construct shows what you wanna do way more clearly — something we can’t ignore as long we write code for humans. Not to mention lower verbosity and shorter code.

So: let’s look at some powerful if statement upgrades.

1. The AND (&&) operator

The && operator, unique to JavaScript.

We it I quickly go from this:

JavaScript
function visitSite(user) { if (user.isLoggedIn) { console.log(`You are ${user.name}`); } console.log('Welcome to Coding Beauty.'); }

To this:

JavaScript
function visitSite(user) { user.isLoggedIn && console.log(`You are ${user.name}`); console.log('Welcome to Coding Beauty.'); }

I’ve eradicated the nested and compacted the branching logic into a one-liner.

You want to use this when there’s an if but no matching else; especially when the if block has only one line.

Even if there are multiple lines you can abstract them into a separate function and apply && again. After all the console.log() in our example is an abstraction itself.

So this:

JavaScript
function visitSite(user) { if (user.isLoggedIn) { console.log(`Welcome back, ${user.name}!`); console.log( `Your last login was on ${user.lastLoginDate}` ); console.log(`Your account type is ${user.accountType}`); } console.log('Welcome to Coding Beauty.'); }

Transforms to this:

JavaScript
function visitSite(user) { user.loggedIn && handleUser(user); console.log('Welcome to Coding Beauty.'); } function handleUser(user) { console.log(`Welcome back, ${user.name}!`); console.log( `Your last login was on ${user.lastLoginDate}` ); console.log(`Your account type is ${user.accountType}`); }

2. Ternary operator

Ternary operators let us compact if-else statements into a one-liner.

They’re great if-else replacements when all conditional cases only involve assigning a value to the same variable.

Here’s an example:

JavaScript
let word; if (num === 7) { word = 'seven'; } else { word = 'unknown'; }

Even though the DRY principle isn’t a hard and fast rule, for this instance things will be much cleaner if we used ternaries to avoid writing the variable assignment twice:

JavaScript
const word = num === 7 ? 'seven' : 'unknown';

Now we can even use const to keep things immutable and pure.

Nested ternaries

And when we have 3 or more branches in the if-else statement or we nest ifs, the cleaner ternary replacement will contain inner ternaries.

So this:

JavaScript
const getNumWord = (num) => { if (num === 1) { return 'one'; } else if (num === 2) { return 'two'; } else if (num === 3) { return 'three'; } else if (num === 4) { return 'four'; } else return 'unknown'; };

Becomes:

JavaScript
const getNumWord = (num) => num === 1 ? 'one' : num === 2 ? 'two' : num === 3 ? 'three' : num === 4 ? 'four' : 'unkwown';

Some people do cry about nested ternaries though, arguing that they’re complicated and cryptic. But I think that’s more of a personal preference, or maybe poor formatting.

And it’s formatting, we have tools like Prettier that have been doing this job (and only this job) for centuries.

Ever had code this badly formatted?

As long as there’s sufficient indentation you should have no problems with readability. If the branching gets much more complex than above you probably want to move some of the lower-level logic into preceding variables or tiny functions.

Speaking of readability, Prettier is changing their nested ternary style; they’ve come up with something quite unique.

The current style uses a clever combination of flat and tree-like nesting; adding further indentation for nested ternaries in the truthy branch, but keeping things flat for those in the in the falsy branch.

JavaScript
const animalName = pet.canSqueak() ? 'mouse' : pet.canBark() ? pet.isScary() ? 'wolf' // Only nests this because it's in the truthy section : 'dog' : pet.canMeow() ? 'cat' : pet.canSqueak() // Flat because it's in the falsy section ? 'mouse' : 'probably a bunny';

But very soon Prettier will format the above like this:

JavaScript
const animalName = pet.canSqueak() ? 'mouse' : pet.canBark() ? pet.isScary() ? 'wolf' : 'dog' : pet.canMeow() ? 'cat' : pet.canSqueak() ? 'mouse' : 'probably a bunny';

The main change is the ?‘s are all now at the ending of the same line of its ending, instead of the next one.

3. Switch statement

You will find this in C-style languages like Java, C#, and Dart — and it looks exactly the same in those languages with a few semantic differences.

If ternaries are best for generating output for one variable, then switch statements are best for processing input *from* one variable:

JavaScript
function processUserAction(action) { switch (action) { case 'play': // if (action === 'play') console.log('Playing the game...'); startGame({ mode: 'multiplayer' }); break; case 'pause': // else if (action === 'pause') console.log('Pausing the game...'); pauseGame(); break; case 'stop': console.log('Stopping the game...'); endGame(); goToMainMenu(); break; case 'cheat': console.log('Activating cheat mode...'); enableCheatMode(); break; default: // else console.log('Invalid action!'); break; } }

The unique power of switch statements comes from being able to omit the break at the end of each case and let execution “fallthrough” to the next case:

JavaScript
// Generate a random number between 1 and 6 to simulate rolling a dice const diceRoll = Math.floor(Math.random() * 6) + 1; console.log(`You rolled a ${diceRoll}!`); switch (diceRoll) { case 1: console.log('Oops, you rolled a one!'); console.log('You lose a turn.'); break; case 2: console.log( 'Two heads are better than one... sometimes' ); case 4: case 6: // else if (diceRoll === 2 || diceRoll === 4 || diceRoll === 6) console.log('Nice roll!'); console.log('You move forward two spaces.'); break; // ... default: console.log('Invalid dice roll.'); }

Most other languages with switch-case allow this, with the notable exception of C#.

4. Key-value object

Key-value objects let us declaratively map inputs to outputs.

JavaScript
const weatherActivities = { sunny: 'go to the beach', rainy: 'watch a movie at home', cloudy: 'take a walk in the park', snowy: 'build a snowman', }; const weather = 'sunny'; console.log(`Okay, so right now it's ${weather}.`); console.log(`Why don't you ${weatherActivities[weather]}?`);

I found this invaluable when creating screens in React Native apps – each with its own loading, error and success states. Here’s a snippet of a screen in one of our apps:

JavaScript
// screens/course-list.tsx // ... import { ActivityIndicator, Text // ... } from 'react-native-paper'; type ViewState = 'loading' | 'error' | 'data'; export function Courses({ route, navigation }) { const [viewState, setViewState] = useState<ViewState>('loading'); const state = useAppState(); const courses = state?.courseList; // ... const contentDict = { loading: <ActivityIndicator />, error: <Text>Error</Text>, data: <CourseList courses={courses} />, }; const content = contentDict[viewState]; return ( <View> <StatusBar /> {content} </View> ); } // ...

Final thoughts

So if statements aren’t bad at all and are great for writing conditional logic in an easily understandable way. But it’s important to consider alternative approaches to make code shorter, clearer and even more expressive. It’s not about eliminating if statements entirely, but rather adopting effective techniques that make our code more efficient and elegant.

How to HACK JavaScript with Well-Known Symbols (5 ways)

They call them well-known symbols – even though most developers have never used them or even heard of them.

They’re a really cool feature you can use to make magic like this happen:

Use well-known symbols to magically redefine JavaScript's core functionalities to behave in unique and delightful ways.

You’ll see how we built the List class with a well-known symbol to do this.

They’re all about completely customizing the normal behavior of built-in operations like for..of. It’s like operator overloading in C++ and C#.

Also all static methods of the Symbol class.

1. Symbol.hasInstance

So first up we have Symbol.hasInstance: for easily changing how the instanceof operator behaves.

JavaScript
const person = new Person({ name: 'Tari Ibaba', at: 'codingbeautydev.com', }); // Because of Symbol.hasInstance console.log(person instanceof Person); // ❌ false (!!) console.log('Tari Ibaba' instanceof Person); // ✅ true console.log('Person' instanceof Person); // ❌ false

Normally instanceof is all about checking if a variable is an instance of class.

JavaScript
class Person { constructor({ name, at }) { this.name = name; this.at = at; } } const person = new Person({ name: 'Tari Ibaba', at: 'codingbeautydev.com', }); console.log(person instanceof Person); // ✅ true console.log('Person' instanceof Person); // ❌ false console.log(person instanceof String); // ❌ false

This is as it should be. Pretty standard stuff.

But with Symbol.hasInstance we can completely transform how instanceof works:

JavaScript
class Person { constructor({ name, at }) { this.name = name; this.at = at; } static [Symbol.hasInstance](obj) { const people = ['Tari Ibaba', 'Ronaldo']; return people.includes(obj); } }

Now a Person is no longer a Person, as far as instanceof is concerned.

JavaScript
const person = new Person({ name: 'Tari Ibaba', at: 'codingbeautydev.com', }); console.log(person instanceof Person); // ❌ false (!!) console.log('Tari Ibaba' instanceof Person); // ✅ true console.log('Person' instanceof Person); // ❌ false

What if we don’t want to completely override it, but instead extend it in an intuitive way?

We can’t use instanceof inside the symbol because that’ll quickly lead to an infinite recursion:

Instead we compare the special constructor property of the object to our own:

JavaScript
class Fruit { constructor(name) { this.name = name; } [Symbol.hasInstance](obj) { const fruits = ['🍍', '🍌', '🍉', '🍇']; // this == this.constructor in a static method return obj.constructor === this || fruits.includes(obj); } } const fruit = new Fruit('apple');

If you’re just hearing of .constructor, this should explain everything:

JavaScript
String.prototype.constructor.prototype.constructor === String // true

2. Symbol.iterator

Our next hack is Symbol.iterator, for totally altering how and if loop works on an object.

Remember this:

We did this thanks to Symbol.iterator:

JavaScript
class List { elements = []; wordEmojiMap = { red: '🔴', blue: '🔵', green: '🟢', yellow: '🟡', }; add(element) { this.elements.push(element); return this; } // Generator *[Symbol.iterator]() { for (const element of this.elements) { yield this.wordEmojiMap[element] ?? element; } } }

We see generators crop up once again.

Any time we use for..of

JavaScript
const numbers = [1, 2, 3]; for (const num of numbers) { console.log(num); } /* 1 2 3 */

This happens behind the scenes:

JavaScript
const iterator = numbers[Symbol.iterator](); // for..of: Keep calling .next() and using value until done console.log(iterator.next()); // Object {value: 1, done: false} console.log(iterator.next()); // Object {value: 2, done: false} console.log(iterator.next()); // Object {value: 3, done: false} console.log(iterator.next()); // Object {value: undefined, done: true}

So with Symbol.iterator we completely changed what for..of does with any List object:

JavaScript
class List { // ... *[Symbol.iterator]() { for (const element of this.elements) { yield this.wordEmojiMap[element] ?? element; } } }
JavaScript
const colors = new List(); colors.add('red').add('blue').add('yellow'); const iterator = colors[Symbol.iterator](); console.log(iterator.next()); // { value: '🔴', done: false } console.log(iterator.next()); // { value: '🔵', done: false } console.log(iterator.next()); // { value: '🟡', done: false } console.log(iterator.next()); // { value: undefined, done: true }

4. Symbol.toPrimitive

With Symbol.toPrimitive we quickly go from this:

To this:

We did this by overriding Symbol.toPrimitive:

JavaScript
class Person { constructor({ name, at, favColor }) { this.name = name; this.at = at; this.favColor = favColor; } [Symbol.toPrimitive]() { return `I'm ${this.name}`; } }

Now we can use a Person object anywhere we use a string for interpolation & concatenation:

JavaScript
const str = 'Person: ' + person; console.log(str); // Person: I'm Tari Ibaba

There’s even a hint parameter that makes an object act like a number, string, or something else.

JavaScript
class Money { constructor(amount, currency) { this.amount = amount; this.currency = currency; } [Symbol.toPrimitive](hint) { if (hint === 'string') { return `${this.amount} ${this.currency}`; } else if (hint === 'number') { return this.amount; } else if (hint === 'default') { return `${this.amount} ${this.currency}`; } } } const price = new Money(500, 'USD'); console.log(String(price)); // 500 USD console.log(+price); // 500 console.log('Price is ' + price); // Price is 500 USD

4. Symbol.split

Genius well-known symbol for turning your custom objects into string separators:

JavaScript
class Greedy { [Symbol.split](str) { return `Me: ${str}, you: 0`; } } class ReadableNumber { [Symbol.split](str) { return str.split('').reduceRight((acc, cur, index, arr) => { return index % 3 === 0 && index < arr.length - 1 ? cur + ',' + acc : cur + acc; }, ''); } } console.log('1-000-000'.split('-')); // [ '1', '000', '000' ] console.log('1000000'.split(new Greedy())); // Me: 1000000, you: 0 console.log('1000000'.split(new ReadableNumber())); // 1,000,000

5. Symbol.search

Just like Symbol.split, transform your custom objects into sophisticated string searching tools:

JavaScript
class Topic { static topics = { 'codingbeautydev.com': ['JavaScript', 'VS Code', 'AI'], }; constructor(value) { this.value = value; } [Symbol.search](where) { const topic = this.constructor.topics[where]; if (!topic) return -1; return topic.indexOf(this.value); } } const str = 'codingbeautydev.com'; console.log(str.search(new Topic('VS Code'))); // 1 console.log(str.search(new Topic('Economics'))); // -1

Final thoughts

From looping to splitting to searching, well-known symbols let us redefine our core functionalities to behave in unique and delightful ways, pushing the boundaries of what’s possibly in JavaScript.

10 amazing web development tools you need to know (#2)

10 more amazing web dev tools to boost your workflow and make development more enjoyable.

From stunning animations to rapid API creation & documentation, these tools will help you get things done faster than ever.

1. Heat.js

Create stunning heat maps and charts with this incredible UI library.

Like this:

GitHub-like heatmap.

Very similar to my GitHub profile (but with way more greens of course):

No external libraries needed, it’s dependency-free.

Customize every detail with a wide range of settings, or choose from over 10 pre-made themes in 40+ languages. Get ready to bring your data to life.

2. Postman

Simply the best for creating and testing APIs.

If you still use curl then you must living in the stone age. Or maybe you have some sort of CLI superiority complex.

Just making a simple POST request is pain; Stressful editing, strict formatting requirements that don’t even stay consistent with the OS and terminal.

1st one works on Linux, 2nd on Windows CMD (I guess), 3rd on Powershell…😴

Why go through any of that when you have a nice and easy GUI with none of these problems?

Body data is easy, query params are easy.

It even has built-in support for testing APIs from Paypal and Google.

3. React Toastify

By far the easiest way I found to add toast notifications to your React app.

All you need is this simple code:

JavaScript
import React from 'react'; import { ToastContainer, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; function App(){ const notify = () => toast("Wow so easy!"); return ( <div> <button onClick={notify}>Notify!</button> <ToastContainer /> </div> ); }

Look it even has all sorts of colorful progress bars to show how much time left before it goes away 👇

And dark mode of course. And different themes for different purposes.

4. GitLens for VS Code

VS Code source control on steroids.

Packed full with tons of views with essential repo data and current file info: file history, commits, branches, remotes, and more.

Even history of particular line in the file, with valuable related data and actions.

5. Palettify

Craft stunning UI color schemes in real-time with Palettify.

That H1 font is awesome by the way.

Play around with shadcn-ui components, toggle light/dark mode, then grab the the CSS theme code with a single click.

Several themes to choose from:

Wiza theme:

6. LinkPreview

I use this to easily get a preview image from a URL.

Like Notion does for their Bookmark block:

Better than depending on unreliable web scraping libraries, or rigid 3rd-party Link Preview UI components.

You just make an API request to this URL:

Plain text
https://api.linkpreview.net/?q=your_url_to_preview

And you easily get your image, along with relevant information for preview.

If we go to the image URL we’ll see it’s the same exact one Notion showed:

7. Lottie

Breathe life into your apps with Lottie.

Lottie takes the magic from Adobe After Effects and brings it straight to your mobile and web apps.

Imagine the experience needed to create this 👇 Then imagine recreating it with raw CSS.

So no more hand-coding – Lottie uses a special plugin to turn those After Effects animations into a lightweight JSON format that works flawlessly on any device.

8. Free Icons

Unleash your creativity with a treasure trove of over 22,000 icons!

Dive into a world of beautiful and free SVG icons, all meticulously tagged and ready to be discovered at your fingertips.

Including dinosaurs like Internet Explorer.

Simply type in a keyword and browse through countless icons to find the perfect visual match for your project.

9. jwt.io

I use this often when having nasty bugs with JSON web token from Firebase Auth or some stuff.

You just paste your token on the left and you instantly see the decoded value on the right.

And using this got me confused at first; I thought JWT was like an encryption where only the receiver could know what was there with the secret?

But no; it turned out JWT is for verifying the source of the message, not hiding information. Like in crypto wallets and transactions private and public keys.

10. TypeSpec

Use this to stop wasting time manually documenting your API for public use.

Describe your data once, and TypeScript magically conjures up everything you need: schema, API specifications, client / server code, docs, and more.

Final thoughts

Use these awesome tools to upgrade your productivity and make development more fun.

Stop using nested ifs: Do this instead

Typical use case for nested ifs: you want to perform all sorts of checks on some data to make sure it’s valid before finally doing something useful with it.

Don’t do this! 👇

JavaScript
function sendMoney(account, amount) { if (account.balance > amount) { if (amount > 0) { if (account.sender === 'user-token') { account.balance -= amount; console.log('Transfer completed'); } else { console.log('Forbidden user'); } } else { console.log('Invalid transfer amount'); } } else { console.log('Insufficient funds'); } }

There’s a better way:

JavaScript
// ✅ function sendMoney(account, amount) { if (account.balance < amount) { console.log('Insufficient funds'); return; } if (amount <= 0) { console.log('Invalid transfer amount'); return; } if (account.sender !== 'user-token') { console.log('Forbidden user'); return; } account.balance -= amount; console.log('Transfer completed'); }

See how much cleaner it is? Instead of nesting ifs, we have multiple if statements that do a check and return immediately if the condition wasn’t met. In this pattern, we can call each of the if statements a guard clause.

If you do a lot of Node.js, you’ve probably seen this flow in Express middleware:

JavaScript
function authMiddleware(req, res, next) { const authToken = req.headers.authorization; if (!authToken) { return res.status(401).json({ error: 'Unauthorized' }); } if (authToken !== 'secret-token') { return res.status(401).json({ error: 'Invalid token' }); } if (req.query.admin === 'true') { req.isAdmin = true; } next(); }

It’s much better than this, right? :

JavaScript
function authMiddleware(req, res, next) => { const authToken = req.headers.authorization; if (authToken) { if (authToken === 'secret-token') { if (req.query.admin === 'true') { req.isAdmin = true; } return next(); } else { return res.status(401).json({ error: 'Invalid token' }); } } else { return res.status(401).json({ error: 'Unauthorized' }); } };

You never go beyond one level of nesting. We can avoid the mess that we see in callback hell.

How to convert nested ifs to guard clauses

The logic for this for doing this is simple:

1. Find the innermost/success if

Here we can clearly see it’s the cond3 if. After this, if we don’t do any more checks and take the action we’ve always wanted to take.

JavaScript
function func(cond1, cond2, cond3) { if (cond1) { if (cond2) { if (cond3) { console.log('PASSED!'); console.log('taking success action...'); } else { console.log('failed condition 3'); } } else { console.log('failed condition 2'); } } else { console.log('failed condition 1'); } }

2. Invert the outermost if and return

Negate the if condition to put the else statements’ body in there and add a return after.

Delete the else braces (keep the body, it still contains the formerly nested ifs, and move the closing if brace to just after the return.

So:

JavaScript
function func(cond1, cond2, cond3) { if (!cond1) { // 👈 inverted if condition // 👇 body of former else clause console.log('failed condition 1'); return; // 👈 exit on fail } // 👇 remaining nested ifs to convert to guard clauses if (cond2) { if (cond3) { console.log('PASSED!'); console.log('taking success action...'); } else { console.log('failed condition 3'); } } else { console.log('failed condition 2'); } }

3. Do the same for each nested if until you reach the success if

And then:

JavaScript
function func(cond1, cond2, cond3) { if (!cond1) { console.log('failed condition 1'); return; } if (!cond2) { console.log('failed condition 2'); return; } // 👇 remaining nested ifs to convert if (cond3) { console.log('PASSED!'); console.log('taking success action...'); } else { console.log('failed condition 3'); } }

And finally:

JavaScript
function func(cond1, cond2, cond3) { if (!cond1) { console.log('failed condition 1'); return; } if (!cond2) { console.log('failed condition 2'); return; } if (!cond3) { console.log('failed condition 3'); return; } console.log('PASSED!'); console.log('taking success action...'); }

I use the JavaScript Booster extension to make inverting if statements in VS Code much easier.

Here we only had to put the cursor in the if keyword and activate the Show Code Actions command (Ctrl + . by default).

Check out this article for an awesome list of VSCode extensions you should definitely install alongside with JavaScript Booster.

Tip: Split guard clauses into multiple functions and always avoid if/else

What if we want to do something other after checking the data in an if/else? For instance:

JavaScript
function func(cond1, cond2) { if (cond1) { if (cond2) { console.log('PASSED!'); console.log('taking success action...'); } else { console.log('failed condition 2'); } console.log('after cond2 check'); } else { console.log('failed condition 1'); } console.log('after cond1 check'); }

In this function regardless of cond1‘s value, the 'after cond1 check' the line will still print. Similar thing for the cond2 value if cond1 is true.

In this case, it takes a bit more work to use guard clauses:

If we try to use guard clauses, we’ll end up repeating the lines that come after the if/else checks:

JavaScript
function func(cond1, cond2) { if (!cond1) { console.log('failed condition 1'); console.log('after cond1 check'); return; } if (!cond2) { console.log('failed condition 2'); console.log('after cond2 check'); console.log('after cond1 check'); return; } console.log('PASSED!'); console.log('taking success action...'); console.log('after cond2 check'); console.log('after cond1 check'); } func(true);

Because the lines must be printed, we print them in the guard clause before returning. And then, we print it in all(!) the following guard clauses. And once again, in the main function body if all the guard clauses were passed.

So what can we do about this? How can we use guard clauses and still stick to the DRY principle?

Well, we split the logic into multiple functions:

JavaScript
function func(cond1, cond2) { checkCond1(cond1, cond2); console.log('after cond1 check'); } function checkCond1(cond1, cond2) { if (!cond1) { console.log('failed condition 1'); return; } checkCond2(cond2); console.log('after cond2 check'); } function checkCond2(cond2) { if (!cond2) { console.log('failed condition 2'); return; } console.log('PASSED!'); console.log('taking success action...'); }

Let’s apply this to the Express middleware we saw earlier:

JavaScript
function authMiddleware(req, res, next) { checkAuthValidTokenAdmin(req, res, next); } function checkAuthValidTokenAdmin(req, res, next) { const authToken = req.headers.authorization; if (!authToken) { return res.status(401).json({ error: 'Unauthorized' }); } checkValidTokenAdmin(req, res, next); } function checkValidTokenAdmin(req, res, next) { const authToken = req.headers.authorization; if (authToken !== 'secret-token') { return res.status(401).json({ error: 'Invalid token' }); } checkAdmin(req, res, next); } function checkAdmin(req, res, next) { if (req.query.admin === 'true') { req.isAdmin = true; } next(); }

In a way, we’ve replaced the if/else statements with a chain of responsibility pattern. Of course, this might be an overkill for simple logic like a basic Express request middleware, but the advantage here is that it delegates each additional check to a separate function, separating responsibilities and preventing excess nesting.

Key takeaways

Using nested ifs in code often leads to complex and hard-to-maintain code; Instead, we can use guard clauses to make our code more readable and linear.

We can apply guard clauses to different scenarios and split them into multiple functions to avoid repetition and split responsibilities. By adopting this pattern, we end up writing cleaner and more maintainable code.