Javascript Promises - Understanding Chaining and Error Handling

Javascript Promises are quite simple to start with, but confusions arise when chained promises are used :

  • Why is then() executed after catch() in a sequence like then().catch().then() ?
  • How can a catch() be assigned to each then(), something like then().catch().then().catch() ?
  • Should a single catch() be used for all the then's() ?

Both then() and catch() Return a Promise

To understand promise chaining and error handling, it is important to know that both then() and catch() return a Promise.

Return Value of then()

When a Promise is resolved, the handler function in then() is executed. The value passed by resolve() is passed as a parameter to the handler.

var promise = new Promise(function(resolve, reject) {
    resolve(55);
});

// callback in then() is executed when promise is resolved
promise.then(function(data) {
	// 55 is the output
	console.log(data);
});

The return value of then() is also a Promise, which is different from the original Promise :

  • If the handler function in then() returns a value, the Promise returned by then() is resolved with the returned value as its value.

    var promise = new Promise(function(resolve, reject) {
        resolve(55);
    });
    
    // handler in then() returns a value
    // then() returns a Promise
    var promise2 = promise.then(function(data) {
    	return data;
    });
    
    // promise2 is resolved
    promise2.then(function(data) {
    	// 55 is the output
    	console.log(data);
    });
    
  • If the handler function in then() throws an error, the Promise returned by then() is rejected with the returned error as its value.

    var promise = new Promise(function(resolve, reject) {
        resolve(55);
    });
    
    // handler in then() throws an error
    // then() returns a Promise
    var promise2 = promise.then(function(data) {
    	throw new Error(100);
    });
    
    // handler will never be called
    promise2.then(function(data) {
    	console.log(data);
    });
    
    // promise2 is rejected
    promise2.catch(function(data) {
    	// Error object is the output
    	console.log(data);
    });
    
  • If the handler function in then() returns an already resolved promise, the Promise returned by then() is resolved with that Promise's return value.

    var promise = new Promise(function(resolve, reject) {
        resolve(55);
    });
    
    // handler in then() returns a Promise
    // then() returns a different Promise
    var promise2 = promise.then(function(data) {
    	return Promise.resolve(20);
    });
    
    // promise2 is resolved
    promise2.then(function(data) {
    	// 20 is the output
    	console.log(data);
    });
    
  • Similarly if the handler function returns an already rejected promise, the Promise returned by then() is rejected with that Promise's return value.

  • If the handler function returns a pending Promise, the Promise returned by then() is resolved or rejected depending upon the returned Promise being resolved or rejected.

    var promise = new Promise(function(resolve, reject) {
        resolve(55);
    });
    
    // handler in then() returns a Promise
    // then() returns a different Promise
    var promise2 = promise.then(function(data) {
    	var p = new Promise(function(resolve, reject) {
    	    setTimeout(function() {
    	    	resolve(75);
    		}, 1000);
    	});
    
    	return p;
    });
    
    // promise2 is resolved
    promise2.then(function(data) {
    	// 75 is the output
    	console.log(data);
    });
    
  • If the handler function returns nothing, the Promise returned by then() is resolved with undefined as its value.

    var promise = new Promise(function(resolve, reject) {
        resolve(55);
    });
    
    // handler in then() returns nothing
    // then() returns a Promise
    var promise2 = promise.then(function(data) {
    	// no return value
    	console.log(data);
    });
    
    // promise2 is resolved
    promise2.then(function(data) {
    	// undefined is the output
    	console.log(data);
    });
    

Return Value of catch()

Since catch(onRejected) is just a wrapper for then(undefined, onRejected), return value of catch() is the same as described for then() in the above section. However for the sake for completeness, it is explained below.

When a Promise is rejected, the handler function in catch() is executed. The value passed by reject() is passed as a parameter to the handler.

var promise = new Promise(function(resolve, reject) {
    reject(55);
});

// callback in catch() is executed when promise is rejected
promise.catch(function(data) {
	// 55 is the output
	console.log(data);
});

The return value of catch() is also a Promise, which is different from the original Promise :

  • If the handler function in catch() returns a value, the Promise returned by catch() is resolved with the returned value as its value.

    var promise = new Promise(function(resolve, reject) {
        reject(55);
    });
    
    // handler in catch() returns a value
    // catch() returns a Promise
    var promise2 = promise.catch(function(data) {
    	return data;
    });
    
    // promise2 is resolved
    promise2.then(function(data) {
    	// 55 is the output
    	console.log(data);
    });
    
  • If the handler function in catch() throws an error, the Promise returned by catch() is rejected with the returned error as its value.

    var promise = new Promise(function(resolve, reject) {
        reject(55);
    });
    
    // handler in catch() throws an error
    // catch() returns a Promise
    var promise2 = promise.catch(function(data) {
    	throw new Error(100);
    });
    
    // handler will never be called
    promise2.then(function(data) {
    	console.log(data);
    });
    
    // promise2 is rejected
    promise2.catch(function(data) {
    	// Error object is the output
    	console.log(data);
    });
    
  • If the handler function returns an already resolved promise, the Promise returned by catch() is resolved with that Promise's return value.

    var promise = new Promise(function(resolve, reject) {
        reject(55);
    });
    
    // handler in catch() returns a Promise
    // catch() returns a different Promise
    var promise2 = promise.catch(function(data) {
    	return Promise.resolve(20);
    });
    
    // promise2 is resolved
    promise2.then(function(data) {
    	// 20 is the output
    	console.log(data);
    });
    
  • Similarly if the handler function returns an already rejected promise, the Promise returned by catch() is rejected with that Promise's return value.

  • If the handler function returns a pending Promise, the Promise returned by catch() is resolved or rejected depending upon the returned Promise being resolved or rejected.

    var promise = new Promise(function(resolve, reject) {
        reject(55);
    });
    
    // handler in catch() returns a Promise
    // catch() returns a different Promise
    var promise2 = promise.catch(function(data) {
    	var p = new Promise(function(resolve, reject) {
    	    setTimeout(function() {
    	    	resolve(75);
    		}, 1000);
    	});
    
    	return p;
    });
    
    // promise2 is resolved
    promise2.then(function(data) {
    	// 75 is the output
    	console.log(data);
    });
    
  • If the handler function returns nothing, the Promise returned by catch() is resolved with undefined as its value.

    var promise = new Promise(function(resolve, reject) {
        reject(55);
    });
    
    // handler in catch() returns nothing
    // catch() returns a Promise
    var promise2 = promise.catch(function(data) {
    	// no return value
    	console.log(data);
    });
    
    // promise2 is resolved
    promise2.then(function(data) {
    	// undefined is the output
    	console.log(data);
    });
    

Example #1

If you have understood the above sections, managing a chain of promises should now be easier.

var promise = new Promise(function(resolve, reject) {
    resolve(55);
});

promise.
	then(function(data) {
		console.log(data);
		var p = new Promise(function(resolve, reject) {
		    setTimeout(function() {
		    	resolve(75);
			}, 1000);
		});

		return p;
	}).
	then(function(data) {
		console.log(data);

		return data + 20;
	}).
	then(function(data) {
		console.log(data);
	});


// output
55
75
95

Promise Rejections are Handled by the Next Available catch() in Line

Whenever a Promise is rejected in a chain, it skips forward to the next available catch(). This works just like handling a catch while writing synchronous code.

var promise = new Promise(function(resolve, reject) {
    reject(55);
});

promise.
	// skipped
	then(function(data) {
		console.log(data);
		var p = new Promise(function(resolve, reject) {
		    setTimeout(function() {
		    	resolve(75);
			}, 1000);
		});

		return p;
	}).
	// skipped
	then(function(data) {
		console.log(data);

		return data + 20;
	}).
	// executed
	catch(function(data) {
		// 55 is the output
		console.log(data);
	}).
	// executed after catch()
	then(function() {
		// end is the output
		console.log('end');
	});

Example #2

var promise = new Promise(function(resolve, reject) {
    reject(55);
});

promise.
	then(function(data) {
		console.log(data);
		var p = new Promise(function(resolve, reject) {
		    setTimeout(function() {
		    	resolve(75);
			}, 1000);
		});

		return p;
	}).
	catch(function(data) {
		console.log(data);
		var p = new Promise(function(resolve, reject) {
		    setTimeout(function() {
		    	resolve(45);
			}, 1000);
		});

		return p;
	}).
	then(function(data) {
		console.log(data);

		throw new Error(data);
	}).
	then(function(data) {
		console.log(data);
	}).
	catch(function(data) {
		console.log(data);

		return Promise.resolve(22);
	}).
	then(function(data) {
		console.log(data);

		return new Promise(function(resolve, reject) {
		    setTimeout(function() {
		    	reject(65);
			}, 1000);
		});
	}).
	catch(function(data) {
		console.log(data);
		return 100;
	}).
	then(function(data) {
		console.log(data)
	});


// output
55
45
Error
22
65
100

Conclusion

Understanding that both then() and catch() return a Promise which will be resolved or rejected, which will then call the next then() or catch() is the key to understanding Promise chaining.