Right now, it's a mystery to me if my try/catch blocks are going to work at all. I set them around code, and then, because something in the code was "asynchronous", which appears to be a fancy way of saying forked to another thread/process at the OS level, the try/catch is ignored if it happens in that code.
I'm fine with that, I just want to know if there's some indication of this? By convention, I'm given to understand, if a call asks for a callback, it's asych, otherwise it's not. I get why a callback means asych, but I'm afraid that the reverse isn't always true: THere's nothing stopping me from surrounding a call with a try/catch that gets loaded into a new call stack and also doens't ask for a callback. This seems really messy to me, and I'd like a little more control over my try/catches than using the default callback that all uncaught exceptions are handled by, if possible.
UPDATE: here is an example:
var UserSchema = new mongoose.Schema({
email: {type: String, unique: true},
password: String,
firstName: String,
lastName: String,
created_at: {type: Date, default: Date.now},
updated_at: Date
});
var User = mongoose.model('User', UserSchema);
var users = [
{email: 'foo@bar.com', firstName: 'Justin', lastName: 'Jones'},
{email: 'foo@bar.com', firstName: 'Justin', lastName: 'Jones'}
];
users.forEach(function(user) {
var newUser = new User(user);
newUser.save(function(err, obj) {
if (!err) console.log("Saved: ", obj.email);
});
});
Given the code above, there's no way to catch an exception inside of save(), as it happens in another call stack. Is there any way for me to externally know that's what's about to happen when I call save()?
UPDATE: Everyone telling me to use handlers for this should maybe read this? It's clearly suggesting not to deal with exceptions that aren't caught in their "thread execution", quotes since it only acts like a thread.
"Asynchronous" is not "a fancy way of saying forked to another thread/process".
JavaScript is single-threaded. End of story. There is no forking at the language level.
"Asynchronous" means just what it says: The order of execution is not the order of the code. Some bits of the code - callback functions - will execute at some point in time, when a certain event occurs. It is an event-based programming model.
Consider this simple example:
function hello () { alert("Hello!"); }
setTimeout(hello, 2000);
This is an asynchronous callback in its most basic form. You have a callback handler - the function hello
- and an event generator, in this instance a time-based one.
Once the event occurs (2s have have passed), the callback handler is called.
Now for a modification:
function hello() { alert(foo.bar); }
try {
setTimeout(hello, 2000);
} catch (ex) {
alert(ex.message);
}
We introduce a try-catch block around setTimeout
. It guards the registration of a callback, nothing more. Most likely this step will succeed, hence the try-catch block will never do anything. The fact that the callback itself will fail in 2 seconds from now does not affect the try-catch block, naturally. This is the behavior you find confusing.
Now for another modification:
function hello() {
try {
alert(foo.bar);
} catch (ex) {
alert("foo.bar is not defined");
}
}
setTimeout(hello, 2000);
Now the try-catch block guards the step that can actually fail. This implies that you must use try-catch blocks where the errors can occur, not generally wrap them around large sections of your program (which is what you seem to do).
But how to get the exception to do do something useful and configuable? By introducing more callbacks, naturally.
function hello(onError) {
try {
alert(foo.bar);
} catch (ex) {
onError("foo.bar is not defined", ex);
}
}
function errorHandler(customMessage, exception) {
alert(customMessage);
// or do something with exception
}
setTimeout(function () {
hello(errorHandler)
}, 2000);
As per your added example:
var saveUsers = function(users, onSuccess, onError) {
users.forEach(function(user) {
var newUser = new User(user);
newUser.save(function(err, obj) {
if (err) {
onError(err, obj);
return false;
} else {
onSuccess(obj);
}
});
});
}
var users = [
{email: 'foo@bar.com', firstName: 'Justin', lastName: 'J'},
{email: 'foo@bar.com', firstName: 'Justin', lastName: 'J'}
];
saveUsers(
users,
function (user) { console.log("Saved: ", user.email); },
function (err, user) { console.log("Could not save: ", user.email); }
});
The semantic is literally just when the function you called has finished executing. If it spawns an I/O process and registers an event, your try-catch
block won't surround that because it's executed on another loop through the implicit Javascript event loop.
The presence or nonexistence of a callback parameter in the function you're executing has no bearing whatsoever on whether or not work started by the function will cause an event to fire off somewhere else. An EventEmitter-based object registers handlers with the .on('eventName', functionName)
mechanism, so multiple events and multiple handlers can be accessing the same "work" but that is all kicked off by a function that takes no callbacks. While the Array
object's forEach
method takes a callback and is synchronous.
Simply put, nothing beyond the event loop barrier should cause a Javascript exception to be thrown. Only the code on the Javascript side of things can. So you put your try-catch
blocks, if needed, on that side: In your function that calls the possibly async code if that function will possibly throw an error, and in your callback function itself if it calls something that could possibly throw an error. If it's async, they are two separate call stacks from Javascript's perspective, so they have different try-catch
scopes; if it's sync, you'll just have one extra set of try-catch
checks, and at least you'll have a better idea of what could have thrown the error.
Personally, I think try-catch just doesn't work in a language like Javascript, and was added to make it more Java-like, so I try to avoid code that uses throw
for Node.js stuff. (Exceptions are if it's using them only for initial configuration of the library/object that can't work or if it's using it [poorly, in my opinion, because of the execution overhead] as an internal way to break out of deep synchronous code and doesn't expose it to me.)
EDIT: To better explain the callstack in Javascript, here's a simple ASCII diagram showing each level of the stack versus time:
== Javascript Event Loop =========================================================
== My Awesome Function ====================================== == My callback ==
== Call to an async library ====================
== I/O request and callback registration ===
Anything that is thrown by My callback
will be sent straight back to the Javascript Event Loop and can only be caught by registering a process.on('uncaughtException', myFunc)
handler to take care of it at that point. Basically, any code of your own can certainly use try-catch
, but it should never throw
if it'll be called directly as an event handler.
As for your recent edit to your question async.forEach
will solve your issue. You pass it the array to iterate over, then the function to execute on each item in the array, and then a "finally"-style function to either handle an error or continue your code from there.
Nathan: I'm not going to go into the bigger question of finding out "when code is going to leave the current stack". I can help you with saving your users though. I recommend the great async library:
function saveUser(user, callback) {
new User(user).save(callback);
}
async.forEach(users, saveUser, function(err, results) {
// Respond
});
If you're simply using callback functions, then it doesn't matter. NodeJS is single-threaded, thus everything is on the same call stack.
BUT! You're right about NodeJS may sometimes "leave" the current call stack. The leave is in quote because it's not really leaving the current call stack but simply returning to the root call (process.nextTick()
). Then on the next tick, it's spawn a "new" call stack.
If you want a semantic, I guess you can say that anywhere that you're using EventEmitter
, you're passing that callback function to the next tick and therefore "moving" it to another call stack. (But even so, it's not entirely true, because EventEmitter actually dispatches event on the current tick).