Demystifying JavaScript Promises – From Basics to Brilliance

Sudeepta GiriSudeepta Giri
8 min read

From callback hell to promise heaven - Your complete guide to mastering asynchronous JavaScript


The Interview Reality Check

Picture this: You're sitting in your dream job interview, palms slightly sweaty, when the interviewer pulls up this code:

console.log('Start');
Promise.resolve().then(() => console.log('Promise'));
setTimeout(() => console.log('Timeout'), 0);
console.log('End');

"What's the output?" they ask with a knowing smile.

If you confidently answered "Start, End, Promise, Timeout" - congratulations! You're already ahead of 70% of candidates. But buckle up, because we're about to dive deep into the world of JavaScript Promises, and by the end of this journey, you'll be the one wearing that knowing smile.


What Exactly is Asynchronous JavaScript?

Before we dive into Promises, let's get crystal clear on what asynchronous JavaScript actually means.

The Simple Explanation:-

Imagine you're downloading a Movie:

Synchronous = You start downloading a movie → stare at the progress bar doing nothing → wait until 100% complete → only then can you start the next download

Asynchronous = You start downloading a movie → get a notification system → continue browsing/working while download happens in the background → notification pops up when ready

Asynchronous JavaScript works the same way:

🔹 Non-blocking: Code execution doesn't stop for time-consuming tasks
🔹 Background processing: Tasks run "behind the scenes"
🔹 Efficient: Your app stays responsive while handling multiple operations

Real-World Async Operations

  • API calls - Fetching data from servers

  • User interactions - Click handlers, form submissions

  • File operations - Reading/writing files

  • Timers - setTimeout, setInterval

  • Media loading - Images, videos, audio

Without async behavior, a single slow API call would freeze your entire webpage! 😱


The Problem: Why Callbacks Became a Nightmare

Meet Callback Hell:-

// The dreaded pyramid of doom!
getData(function(a) {
    getMoreData(a, function(b) {
        getEvenMoreData(b, function(c) {
            getFinalData(c, function(d) {
                // Finally do something with d
                console.log(d);
            });
        });
    });
});

But the real problem isn't just ugly code - it's Inversion of Control:

The Control Problem, aka “Inversion of Control”:-

When using callbacks, you're essentially saying: "Hey, external function, here's my code. Please call it when you're done... hopefully."

What could go wrong?

Never called - Your callback might never execute (hanging program)
Called multiple times - Unexpected behavior and bugs
Called too early/late - Timing issues
Error handling chaos - No standardized way to handle failures

Enter Promises: The Hero We Needed

According to MDN:

"A Promise is an object representing the eventual completion or failure of an asynchronous operation."

But here's the critical interview knowledge that trips up most developers:


The ECMA Standard Truth:-

HOT TAKE: Promises are NOT asynchronous!

Wait, what? Let me blow your mind:

Promises are synchronous objects that execute immediately when created. This is because JavaScript follows the ECMAScript specification, which defines Promise objects as part of the core language features that execute synchronously.ECMAScript® 2026 Language Specification

The asynchronous magic comes from:

  • Operations inside them (setTimeout, fetch, network calls)

  • Promise handlers (.then(), .catch()) which execute asynchronously

console.log('Before Promise');
const promise = new Promise((resolve) => {
    console.log('Inside Promise Constructor'); // This runs IMMEDIATELY
    resolve('Done');
});
console.log('After Promise Creation');

// Output:
// Before Promise
// Inside Promise Constructor  
// After Promise Creation

This is a very crucial thing to understand !!


Promise States: The Lifecycle

A Promise exists in one of three states:

PENDING - Initial state, operation in progress
FULFILLED - Operation completed successfully
REJECTED - Operation failed

const promise = new Promise((res,rej)=>{
    let num = Math.floor(Math.random()*10);
    if(num%2===0){
        res(num);
        console.log("It will Execute");
        rej(num);
        console.log("It will also Execute");
    }else{
        rej(num);
        console.log("It will Execute");
        res(num);
        console.log("It will also Execute");
    }
});

promise.then( val => console.log(val))
    .catch(val => console.log(val));

    // If num = 4
    // res(num): Promise resolves with 4
    // Executes: "It will Execute"
    // rej(num): IGNORED - Promise already resolved
    // Executes: "It will also Execute"

Key Point: Once fulfilled or rejected, a Promise state is immutable - it can never change again!


🛠️ Promise Methods: Your Async Toolkit

The Big Three Methods

.then() - Handles success (and optionally failure)

promise.then(
    value => console.log('Success:', value),    // Success handler
    error => console.log('Error:', error)       // Error handler (optional)
);

.catch() - Handles errors

promise.catch(error => console.log('Caught:', error));

.finally() - Always executes

promise.finally(() => console.log('Cleanup complete'));

Advanced: Static Methods

  • Promise.all() - Wait for all promises (fails if any fail)

  • Promise.allSettled() - Wait for all promises (never fails)

  • Promise.race() - First to settle wins

  • Promise.any() - First to resolve wins (ignores rejections)


Disadvantages of Promise

1. Complexity in Error Handling

promise
    .then(result => processResult(result))
    .then(processed => saveResult(processed))
    .catch(error => {
        // Which operation failed? Hard to tell!
        console.log('Something went wrong:', error);
    });

Solution: Use specific error handling for critical operations.

2. The Debugging Challenge

function fetchData() {
    return new Promise((resolve,reject) => {
        setTimeout(() => {
            reject('Error: Network issue');
        }, 1000);
    });
}

fetchData()
    .then(data => {
        console.log(data);
    })
    .catch(error => {
        console.log('Caught an error:', error); //This will execute first
    })
    .then(() => {
        console.log('This will not run'); // This will also execute which is problematic
    })
    .catch(error => {
        console.log(error);
    });

Solution: Preserve original error information or use proper error wrapping.

3. No Native Cancellation

// Once started, this Promise can't be cancelled
const longRunningPromise = new Promise((resolve) => {
    setTimeout(resolve, 10000); // 10 seconds
});

// User navigates away, but Promise still runs!

Solution: Use AbortController with fetch, or implement custom cancellation tokens.


Let’s Practice Some Tricky Questions

Question 1: Promise Chain with Error Handling

const promiseChain = new Promise((resolve, reject) => {
    console.log('Step 1');
    resolve(1);
})
.then((value) => {
    console.log('Step 2:', value);
    return value + 1;
})
.then((value) => {
    console.log('Step 3:', value);
    throw new Error('Error in Step 3');
})
.then((value) => {
    console.log('Step 4:', value);
    return value * 2;
})
.catch((err) => {
    console.log('Caught:', err.message);
    return 5;
})
.then((value) => {
    console.log('Step 5:', value);
    return value + 1;
});

console.log('End of Code');

Your Answer (Think before revealing!):

Click to see the answer
Step 1 End of Code Step 2: 1 Step 3: 2 Caught: Error in Step 3 Step 5: 5

Explanation:

  1. "Step 1" - Promise constructor executes synchronously

  2. "End of Code" - Synchronous code continues immediately

  3. "Step 2: 1" - First .then() executes asynchronously

  4. "Step 3: 2" - Second .then() gets value + 1 = 2

  5. Error thrown - Skips Step 4, jumps to .catch()

  6. "Caught: Error in Step 3" - Error handled, returns 5

  7. "Step 5: 5" - Chain continues with the returned value

Key Learning: Once caught, the promise chain continues normally!


Question 2: The Tricky Promise.any Race

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => resolve('A'), 100);
});

const p2 = new Promise((resolve, reject) => {
    setTimeout(() => reject('B'), 50);
});

const p3 = new Promise((resolve, reject) => {
    setTimeout(() => resolve('C'), 150);
});

Promise.any([p1, p2, p3])
    .then(value => console.log(value))
    .catch(error => console.log(error));

Your Answer:

Click to see the answer
A

Explanation:

Timeline Breakdown:

  • t=50ms: p2 rejects with 'B' - But Promise.any ignores rejections!

  • t=100ms: p1 resolves with 'A' - WINNER!

  • t=150ms: p3 would resolve with 'C' - Too late!

The Critical Difference:

  • Promise.race: First to settle (resolve OR reject)

  • Promise.any: First to resolve (ignores rejections)

Movie Download Analogy: You're downloading from 3 servers. Server 2 fails immediately, but Promise.any waits for the first successful download!


Question 3: Async Function with Try-Catch

async function asyncFunc() {
    try {
        const result1 = await Promise.resolve('Start');
        console.log(result1);

        const result2 = await new Promise((resolve, reject) => {
            setTimeout(() => reject('Error'), 100);
        });

        console.log(result2); // This line...
    } catch (error) {
        console.log('Caught:', error);
    }

    return 'End';
}

asyncFunc().then(result => console.log(result));

Your Answer:

Click to see the answer
Start Caught: Error End

Explanation:

  1. "Start" - First await resolves successfully

  2. 100ms wait - Function pauses for second Promise

  3. Promise rejects - Second await throws, skipping console.log(result2)

  4. "Caught: Error" - Try-catch handles the rejection

  5. Function continues - Execution resumes after catch block

  6. "End" - Function returns normally

💡 Key Learning: async/await with try-catch provides clean error handling!


Question 4: The Mind-Bender - Two Handler Functions

new Promise((resolve, reject) => {
    if (5 === "5") reject("Type mismatch")
    else resolve("All good")
})
.then(d => {
    console.log("Checkpoint 4", d);
    throw new Error(20);
    return d * 5;
})
.then(d => {
    console.log("Checkpoint 2", d);
    return d;
}, d => {
    console.log("Checkpoint 5", d.message);
    return d.message * 2;
})
.catch(e => {
    console.log("Checkpoint 3", e.message);
    return e.message * 2;
})
.finally(d => {
    console.log("Checkpoint 1", d);
    return d * 5;
})
.then(d => {
    console.log("Checkpoint 6", d);
    return d * 5;
});

Your Answer (This is the tricky one!):

Click to see the answer
Checkpoint 4 All good Checkpoint 5 20 Checkpoint 1 undefined Checkpoint 6 NaN

Explanation:

  1. 5 === "5" is false (strict equality), so resolve("All good")

  2. "Checkpoint 4 All good" - First .then() executes

  3. throw new Error(20) - Creates an error object

  4. Second .then() has TWO functions - The second function (error handler) executes!

  5. "Checkpoint 5 20" - d.message is "20", "20" * 2 = NaN

  6. .catch() is skipped - Error was already handled!

  7. "Checkpoint 1 undefined" - .finally() parameter is always undefined

  8. "Checkpoint 6 NaN" - Continues with NaN

🔑 CRITICAL INTERVIEW INSIGHT: .then() can take two functions:

  • First: handles resolve

  • Second: handles reject (like a local .catch())


💬 Your Turn!

Which Promise concept challenged you the most? Share your experience in the comments below!

And if this guide helped you ace an interview or understand a tricky concept, I'd love to hear about it. Let's build a community of JavaScript masters!

Happy coding, and may your promises always resolve!


Found this helpful? Follow me for more deep-dive JavaScript content and interview preparation guides!

Tags

#JavaScript #Promises #AsyncProgramming #TechnicalInterview #WebDevelopment #Coding #Interview #FAANG

0
Subscribe to my newsletter

Read articles from Sudeepta Giri directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sudeepta Giri
Sudeepta Giri

I’m Sudeepta Giri, a Full Stack Software Engineer with hands-on experience as a Jr. Software Engineer Intern at EPAM Systems, where I built scalable MEAN stack applications, microservices, and optimized frontend performance. I’ve led Agile teams, developed impactful projects like a Rent-Car platform, Mental Health app, and an AI Interview platform, and achieved recognition as a Smart India Hackathon 2022 winner. Passionate about web development, cloud, and problem-solving, I aim to create user-focused and scalable digital solutions.