meteor: guards
A healthy dose of defensive programming is probably a wise idea for most JavaScript code, but it's often critical when working with meteor. In this post I'll examine one of the most common errors that meteor developers encounter.
You'll know what I'm talking about if you have ever seen something like this in your console:
Uncaught TypeError: Cannot read property 'message' of undefined
Let's start with a simple example:
Posts = new Meteor.Collection('posts');
if (Meteor.isClient) {
Template.topPost.helpers({
message: function() {
var post = Posts.findOne();
return post.message;
}
});
}
We defined a Posts
collection and tried to render a message. This looks innocent enough, but we'll get an error because we forgot to add any posts to the collection. The findOne
will return undefined
, and therefore post.message
won't work.
Let's fix this problem by adding a new post on the server:
if (Meteor.isServer) {
Meteor.startup(function() {
if (Posts.find().count() === 0) {
Posts.insert({message: 'hello world'});
}
});
}
And after starting back up, we still get an error! To understand what's going on, let's consider the following order of events:
t=0: The client connects to the server and activates a subscription for posts.
t=1: The client begins rendering the
topPost
template.t=2: The client executes
message
and sees that there are no posts in the database (minimongo).t=N: The server publishes the post document(s) to the client.
The critical observation is that the data you want may not be available on the client when it is first used. This could be true for a number of reasons including (but not limited to):
- The documents do not exist in the database.
- The documents have not been published (or subscribed to).
- The documents have not yet arrived on the client.
A simple solution is to add a guard expression in front of post.message
to ensure that post
is defined.
Template.topPost.helpers({
message: function() {
var post = Posts.findOne();
return post && post.message;
}
});
This works because templates in meteor form a reactive context so the message
function will be evaluated again if a post arrives sometime later.
It's worth mentioning that iron-router will help solve (3) and the subscription half of (2) from the above cases. Specifically, you can use waitOn
in your routes to activate a subscription and avoid rendering the dependent template until the subscription has been marked as ready. While iron-router goes a long way towards solving the problem, it doesn't fix the case where the data simply isn't there.
Hopefully this brief explanation will arm developers with enough information to quickly fix many of these errors. Happy debugging!