0

I'm trying to wrap my head around using async/await with promises and mongoose references.

The code below almost works: the 3 posts get created and so do the comments for the first one, but none of the comments for the third post get created.

I'm assuming this is because I'm not putting the async/awaits in the right place, but I don't know how to organize the code so it works.

const seedData = [
  {
    title: 'Post 1',
    content: `beard sustainable Odd Future pour-over Pitchfork DIY fanny pack art party`,
    comments: [
      { content: 'comment1 1' },
      { content: 'comment1 2' },
      { content: 'comment1 3' }
    ]
  },
  {
    title: 'Post 2',
    content: `flannel gentrify organic deep v PBR chia Williamsburg ethical`,
    comments: []
  },
  {
    title: 'Post 3',
    content: `bag normcore meggings hoodie polaroid gastropub fashion`,
    comments: [{ content: 'comment3 1' }, { content: 'comment3 2' }]
  }
];

async function createPostsAndComments() {
  let promises = [];

  // delete existing documents
  Post.remove(() => {});
  Comment.remove(() => {});

  // create three posts along with their comments
  seedData.forEach(async postData => {
    // create the post
    let post = new Post({
      _id: new mongoose.Types.ObjectId(),
      title: postData.title,
      content: postData.content
    });

    // wait for the promise returned by `post.save`
    promises.push(
      post.save(error => {
        if (error) console.log(error.message);

        // create the comments of the current post
        postData.comments.forEach(async commentData => {
          const comment = new Comment({
            content: commentData.content,
            post: post._id
          });

          // wait for the promise from `comment.save`
          promises.push(
            comment.save(error => {
              if (error) console.log(error.message);
            })
          );
        });
      })
    );
  });
  return Promise.all(promises);
}

async function initSeed() {
  mongoose.connect(process.env.DATABASE, { useMongoClient: true });
  await createPostsAndComments();
  mongoose.connection.close();
}

initSeed();

In case it's useful, here are the schemas:

import mongoose from 'mongoose';

exports.commentSchema = new mongoose.Schema(
  {
    content: {
      type: String,
      required: true
    },
    post: { type: mongoose.Schema.Types.ObjectId, ref: 'Post' }
  },
  {
    toJSON: { virtuals: true }, // TODO: what's this?
    toObject: { virtuals: true }
  }
);
exports.Comment = mongoose.model('Comment', exports.commentSchema);

import mongoose from 'mongoose';

exports.postSchema = new mongoose.Schema({
  _id: mongoose.Schema.Types.ObjectId,
  title: {
    type: String,
    required: true
  },
  content: {
    type: String,
    required: true
  },
  comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }]
});
exports.Post = mongoose.model('Post', exports.postSchema);

1 Answer 1

2

You need to actually await the async calls that return a promise. Like .remove() and .save() and basically every single interaction with the database. We can also make some things simpler with Promise.all():

// <<-- You use async because there are "inner" awaits.
// Otherwise just return a promise
async function createPostsAndComments() {   
  let promises = [];

  // delete existing documents  <<-- This is actually async so "await"
  await Promise.all([Post,Comment].map(m => m.remove()));

  // create three posts along with their comments <<-- Not actually async
  seedData.forEach(postData => {
    // create the post
    let post = new Post({
      _id: new mongoose.Types.ObjectId(),
      title: postData.title,
      content: postData.content
    });

    // wait for the promise returned by `post.save`   <<-- You mixed a callback here
    promises.push(post.save());
    // create the comments of the current post  // <<-- Again, not async
    postData.comments.forEach(commentData => {
      const comment = new Comment({
        content: commentData.content,
        post: post._id
      });
      // <<-- Removing the callback again
      promises.push(comment.save())
    });
  });
  return Promise.all(promises);
}

// Just write a closure that wraps the program. async allows inner await
(async function()  {
  try {
    const conn = await mongoose.connect(process.env.DATABASE, { useMongoClient: true }); // <-- Yep it's a Promise
    await createPostsAndComments();
  } catch(e) {
    console.error(e);
  } finally {
    mongoose.disconnect();
  }
})();

Alternately just make the .remove() calls part of the promises array as well, and now there is not need for async/await within that function. It's just promises anyway:

function createPostsAndComments() {   
  let promises = [];

  //await Promise.all([Post,Comment].map(m => m.remove()));
  promises = [Post,Comment].map(m => m.remove());

  // create three posts along with their comments <<-- Not actually async
  seedData.forEach(postData => {
    // create the post
    let post = new Post({
      _id: new mongoose.Types.ObjectId(),
      title: postData.title,
      content: postData.content
    });

    // wait for the promise returned by `post.save`   <<-- You mixed a callback here
    promises.push(post.save());
    // create the comments of the current post  // <<-- Again, not async
    postData.comments.forEach(commentData => {
      const comment = new Comment({
        content: commentData.content,
        post: post._id
      });
      // <<-- Removing the callback again
      promises.push(comment.save())
    });
  });
  return Promise.all(promises);
}

Or even just await everything instead of feeding to Promise.all:

async function createPostsAndComments() {   

  await Promise.all([Post,Comment].map(m => m.remove()));

  for( let postData of seedData ) {
    // create the post
    let post = new Post({
      _id: new mongoose.Types.ObjectId(),
      title: postData.title,
      content: postData.content
    });

    await post.save());

    for ( let commentData of postData.comments ) {
      const comment = new Comment({
        content: commentData.content,
        post: post._id
      });

      await comment.save())
    }
  }

}

Those seem to be the concepts you are missing.

Basically async is the keyword that means the "inner block" is going to use await. If you don't use await then you don't need to mark the block as async.

Then of course any "Promise" needs an await in place of any .then(). And just don't mix callbacks with Promises. If you really need to, then you can wrap callback style returns in a Promise, but your code here does not need them.

The other main consideration is "error handling". So all that we need do using async/await keywords is implement try..catch as is shown. This is preferable to .catch() in Promise syntax, and essentially defers all errors thrown from the "inner" function to the "outer" block and reports the error there.

So there is no need to add error handling "inside" the createPostsAndComments() function, since that function itself is called within a try..catch.

Sign up to request clarification or add additional context in comments.

3 Comments

Thank you for that excellent response. I learned so much! I think I got confused because in the mongoose docs, they create story1 inside the author.save callback
I think remove() returns a Query not a promise, so I had to remove the await and also add an empty callback: [Post, Comment].map(model => model.remove(() => {})); Otherwise it doesn't work
@nachocab "Everything" returns a promise. This is code I run in production all of the time, and it's also a really common statement in answers of mine here, because it's a typical method for clearing collections on start for demonstrations. You're doing it wrong, I'm doing it right. That's the common theme of the answer you need to learn. If you have additional questions then Ask a new Question. I know it's tempting to keep chatting to someone who answered a question, but you don't do that here.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.