New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Invalid anti-pattern premise #814
Comments
It's code redundancy |
Maybe we should relax the language a bit. It's an antipattern only in certain siturations. |
Yes, and saying:
That syntax changes the logic completely, and it is needed all the time. And it cannot be replaced by So, here's my real-life example that executes a database transaction: var cbData, cbReason, success;
obj.none('begin')
.then(function () {
return callback()
.then(function (data) {
cbData = data; // save callback data;
success = true;
return obj.none('commit'); // COMMIT;
}, function (reason) {
cbReason = reason; // save callback failure reason;
return obj.none('rollback'); // ROLLBACK;
})
.then(function () {
if (success) {
update(false, true, cbData); // client notification;
return $p.resolve(cbData); // resolve with data from the callback;
} else {
update(false, false, cbReason); // client notification;
return $p.reject(cbReason); // reject with reason from the callback;
}
});
}); So, if we replace that middle section with obj.none('begin')
.then(function () {
return callback()
.then(function (data) {
cbData = data; // save callback data;
success = true;
return obj.none('commit'); // COMMIT;
})
.catch(function (error) {
cbReason = error; // save callback failure reason;
return obj.none('rollback'); // ROLLBACK;
})
.then(function () {
if (success) {
update(false, true, cbData); // client notification;
return $p.resolve(cbData); // resolve with data from the callback;
} else {
update(false, false, cbReason); // client notification;
return $p.reject(cbReason); // reject with reason from the callback;
}
});
}); this changes the logic completely. In the initial piece of code we get into the rejection section only if |
There is edit button on the top right corner |
I have updated my previous post with an example. |
How would you write it in synchronous code then? |
Not sure, haven't tried yet, it was written with promises from the beginning. Surely it would be awkward with all the nested callbacks, considering it has to rely on an asynchronous library. |
Those global variables are horrible and seem unnecessary to me. Regardless, code like the following is perfectly reasonable: obj.none('begin').then(function() {
Promise.resolve()
.then(callback)
.then(function(data) {
return obj.none('commit') // COMMIT;
.then(function() {
update(false, true, data); // client notification;
return data;
});
}, function(reason) {
return obj.none('rollback') // ROLLBACK;
.then(function() {
update(false, false, reason); // client notification;
throw reason;
});
}); // TODO: handle commit/rollback fail
}); // TODO: handle begin fail There is no JavaScript syntax for synchronous branching on fails. Regardless, with a pythonic obj.none('begin');
try {
callback();
} catch(reason) {
obj.none('rollback'); // ROLLBACK;
update(false, false, reason); // client notification;
throw reason;
} else {
obj.none('commit'); // COMMIT;
update(false, true, data); // client notification;
return data;
}
// TODO: handle begin/commit/rollback fail Again, the error monad wins :-) |
Can't really do that, your code merges errors from I honestly tried to refactor into using |
@vitaly-t Ah, right, that was the catch, you'll need that nesting here indeed. See the edit to my comment (it doesn't really change the point). |
@vitaly-t your example code is not really ideal for other reasons, as @bergus mentioned - it relies on out of band signalling. Here's how I'd write your example: obj.none('begin')
.then(callback)
.catch(function (err) {
return obj.none('rollback').then(function () {
update(false, false, err); // client notification
throw err;
});
})
.then(function (data) {
return obj.none('commit').return(data);
})
.then(function (data) {
update(false, true, data); // client notification
return data;
}); |
@phpnode not quite, it shows the same problem as what bergus had: within the |
@vitaly-t that's true, this does seem like a scenario where the second argument might be an appropriate option. However I still think that most of the time it really is an anti pattern. I recently started work on a codebase where this is used a lot and it's causing them a lot of pain because it doesn't catch errors that they expect. |
We all have different experience in using promises, depending on the exact tasks at hand. My experience suggests that my example isn't that rare at all. This is why I suggested to reconsider advocating the |
Do Petka's exercise of writing the code as if all operations were synchronous and the correct way will reveal itself. |
@benjamingr I wouldn't even know how to approach this, given that it is built on top of an asynchronous callback-based protocol. For the same reason I'm not sure how practical an exercise this would make. |
Although the |
@vitaly-t assume the callback base protocol returns values instead or taking callbacks and that all methods return synchronous simple values. |
@benjamingr I have tried to think it that way, nothing came up yet, though I will try again. And if promises are to stand for simplicity and streamlined logic, in my example it is isn't there, or perhaps doesn't exist to begin with :) @stefanpenner you first stated the obvious, and then ended with plain trolling, that's not helping here. |
What's wrong with this @vitaly-t ? function transact(fn){
return obj.none("begin").then(fn).then(x => obj.none("commit").return(x),
e => obj.none("rollback").throw(SqlError(e));
}
transact(callback).tap(data => {
update(false, true, data);
}).catch(SqlError, e => {
update(false, false, cbReason);
throw e;
}); // .catch(CommitError, e => /* handle error in commit */) |
@vitaly-t I'm glad you agree, if what I said was obvious that re-affirms the anti-pattern claim. The Stockholm syndrome comment isn't trolling, it's unfortunately true. |
@benjamingr return obj.none("begin").then(fn).then(x => obj.none("commit").return(x),
e => obj.none("rollback").throw(SqlError(e)); as I stated twice above, your @stefanpenner you are not really trying to follow the discussion, perhaps best not to post then ;) |
@vitaly-t Oh, I didn't realize none could throw, that's super easy to fix (I thought you were talking about commit erroring vs callback). function transact(fn){
return obj.none("begin").catch(e => { throw TransactionStartError(e) })
.then(fn).then(x => obj.none("commit").return(x),
e => isNotBeginError(e) ? reject(e) : obj.none("rollback").
throw(SqlError(e));
}
transact(callback).tap(data => {
update(false, true, data);
}).catch(BeginTransactionError, e => {
}).catch(SqlError, e => {
update(false, false, cbReason);
throw e;
}); // .catch(CommitError, e => /* handle error in commit */) Of course we can use a coroutine here and get clearer and more verbose syntax: const transact = coroutine(function*(fn){
try {
var begin = yield obj.none("begin");
} catch (e) { throw new TransactionStartError(e); }
try {
yield fn(); // the transaction not being passed around is weird btw
} catch(e) { return obj.none("rollback").throw(SqlError(e)); }
yield obj.none("commit");
} |
Well, if we take this code from your example: .then(x => obj.none("commit").return(x),
e => isNotBeginError(e) ? reject(e) : obj.none("rollback").throw(SqlError(e)); that's in fact the very same |
How is that even remotely close to the code that is wrapped in a promise constructor? Note the I think what you were missing is that you can conditionally catch things and that in JavaScript you can rethrow errors safely. |
From what I'm reading, this code: .then(x => obj.none("commit").return(x),
e => isNotBeginError(e) ? reject(e) : obj.none("rollback") translates into something like: then(function (x) {
return obj.none("commit").return(x);
}, function (e) {
return isNotBeginError(e) ? reject(e) : obj.none("rollback");
}); except I don't even know what |
Maybe an The key here is merely that you'd rethrow the exception to signal it wasn't handled - preferably you'd wrap it in a more concrete type to signal what sort of exception it is and how someone should handle it. Half the point of exceptions (vs. using return values for errors) is the fact you can write this sort of code and propagation happens rather than writing the code you wrote above in your original example. |
I don't have a goat. LOL :) My code was in fact written to work within the Promises/A+ standard, without any specific promise library in mind, so I'm not being picky here ;) At this point I probably will just leave a perfectly functioning code as is, because I believe that if I need to write extra code, like re-throwing exceptions, then my original code is way better, as it is simpler. And as far as the anti-patterns go, it is still very unconvincing. |
Bluebird contains a lot of sugar that makes developer lives easier - but nothing too specific in this regard - everything above can be written just fine without bluebird. It is considerably less code, and you have a lot less room for mistakes because you don't have to manually handle cases and promises don't stay pending forever or silence errors (because control flows automatically rather than manually). The code still ends up being clearer, less redundant and shorter, but I guess clearer is subjective. I think the |
I'd also like to note that the pattern is there for very obvious cases of abuse, bluebird has a philosophy of doing things that cater for 95% of developers or 95% of use cases. There are always pathological examples where an anti pattern might be appropriate (also for explicit construction). That document was written for things like:
Where the fact "things" happen with the callback if it throws is implicit, and it would have been a lot better to just return a promise and chain off that. This is extremely common - for example in Angular when working against I think your (initial) attempt at the code contains (what I consider) smell, but it is a lot more debatable than the code people usually write with |
Just to return to your very first post here:
Redundancy in code is defined as a repeated block of code whilst performing the exact same operation. Using |
That only works the same as
Actually However
…by Just using |
In the following anti-pattern chapter you are going way off on a limb, only to confuse people:
The .then(success, fail) anti-pattern
The word anti-pattern in programming refers to code redundancy or compromised algorithmic efficiency.
What you refer to is simply a coding style, which got nothing to do with anti-patterns.
The text was updated successfully, but these errors were encountered: