I created a NodeJS program (with Bluebird as Promise library) that handles some validations similar to how the snippet below works, but if I run that script it throws the following error:
Unhandled rejection RangeError: Maximum call stack size exceeded
Apparently, it's doing some recursive function call at the reassigning of the validations functions where I used .bind(ctx)
The way I solved that problem was assigning the Promise factory to obj._validate
instead of reassigning obj.validate
and use _validate(ctx)
where it's needed.
But I still don't realize why that error happened. Can someone explain to me?
// Example validation function
function validate(pass, fail) {
const ctx = this
Promise.resolve(ctx.value) // Simulate some async validation
.then((value) => {
if (value === 'pass') pass()
if (value == 'fail') fail('Validation failed!')
})
}
let validations = [
{name: 'foo', validate: validate},
{name: 'bar', validate: validate},
{name: 'baz', validate: validate},
{name: 'qux', validate: validate}
]
// Reassigning validate functions to a promise factory
// to handle async validation
validations.forEach(obj => {
obj.validate = (ctx) => { // ctx used as context to validation
return new Promise(obj.validate.bind(ctx))
}
})
function executeValidations(receivedValues, validations) {
receivedValues.forEach((obj, i) => {
validations[i].validate(obj) // obj becomes the context to validate
.then(() => console.log('Validation on', obj.name, 'passed'))
.catch(e => console.error('Validation error on', obj.name, ':', e))
})
}
let receivedValues1 = [
{name: 'foo', value: 'pass'},
{name: 'bar', value: 'fail'},
{name: 'baz', value: 'fail'},
{name: 'qux', value: 'pass'},
]
executeValidations(receivedValues1, validations)
let receivedValues2 = [
{name: 'foo', value: 'pass'},
{name: 'bar', value: 'pass'},
{name: 'baz', value: 'fail'},
{name: 'qux', value: 'fail'},
]
executeValidations(receivedValues2, validations)
<script src="//cdn.jsdelivr.net/bluebird/3.4.7/bluebird.js"></script>
EDIT: I think this is a short version of the problem
function fn(res, rej) { return this.foo }
fn = function(ctx) { return new Promise(fn.bind(ctx))}
const ctx = {foo: 'bar'}
fn(ctx)
.then(console.log)
<script src="//cdn.jsdelivr.net/bluebird/3.4.7/bluebird.js"></script>
obj.validate.bind(ctx)
evaluates to an exotic function object with its this
value set to ctx
. It is still, very much, a function object.
It then appears that
obj.validate = (ctx) => { // ctx used as context to validation
return new Promise(obj.validate.bind(ctx))
sets obj.validate
to a function which returns a promise which during its construction synchronously calls its resolver function obj.validate.bind(ctx)
(a.k.a. "executor function" in ES6) which returns a promise object whose construction synchronously calls obj.validate.bind(ctx)
, and so on ad infinitum or the JavaScript engine throws an error.
Hence calling obj.validate
a first time initiates an infinite loop of promise prodution by the resolver function.
A further issue with bind
usage:
Arrow functions bind their lexical this value when declared. Syntactically Function.prototype.bind
can be applied to an arrow function but does not change the this
value seen by the arrow function!
Hence obj.validate.bind(ctx)
never updates the this
value seen within obj.validate
if the method was defined using an arrow function.
The biggest problem may be overwriting the value of the function which performs the operation:
As posted:
validations.forEach(obj => {
obj.validate = (ctx) => { // ctx used as context to validation
return new Promise(obj.validate.bind(ctx))
}
overwrites the validate
property of each validations
entry. This property used to be the named function validate
declared at the start, but is no longer.
In the short version,
function fn(res, rej) { return this.foo }
fn = function(ctx) { return new Promise(fn.bind(ctx))}
const ctx = {foo: 'bar'}
fn(ctx)
fn = function...
overwrites the named function declaration of fn
. This means that when fn
is called later, the fn
of fn.bind(ctx)
refers to the updated version of fn
, not the original.
Note also that the resolver function must call its first function parameter (resolve
) to synchronously resolve a new promise. Return values of the resolver function are ignored.