0

I'm new to js. I've read many of the prior stack overflow posts on asynchronicity but still don't understand the question below.

I have the following code to upload an image file to an S3 bucket. The key thing the code needs to achieve is to have the image1 variable in the parent store the information in the data variable from the s3.upload call in the child function.

My code using promises below works fine. My understanding is that the same thing can be done using callbacks only, and I've been trying to rewrite the code below with callbacks only as a learning exercise, but it has not worked. How would I change this code to do the same thing with callbacks and no promises?

Parent function:

try {
      image1 = await uploadToAWSBucket(file);
}
catch (error) {
      return next(error);
}

Child:

const uploadToAWSBucket = async (fileObject) => {
  let randomFileName = shortid.generate();
  const AWSUploadObject = {
    Bucket: BUCKET_NAME,
    Key: randomFileName,
    Body: fileObject.buffer,
    ContentType: fileObject.mimetype,
  };

  return new Promise((resolve, reject) => {
    s3.upload(AWSUploadObject, (err, data) => {
      if (err) {
        return reject(err);
      }
      return resolve(data);
    });
  });   
};
Neil S
  • 41
  • 2
  • You are actually doing it already, the s3.upload() is using a callback with (err, data) parameter. In your example you converted the handling of the callback into a Promise. For pure callback handling, all you need to do is figure out what you want to do with the err & data when s3.upload() call is done. – DJ. Jun 28 '20 at 22:28

4 Answers4

1

At first, you need to add a callback arg to you async function, removing async keyword

const uploadToAWSBucket = (fileObject, callback) => {

Next, you need to handle s3 response in a callback manner replacing Promise with callback usage.

s3.upload(AWSUploadObject, (err, data) => {
  if (err) {
    callback(err);
    return;
  }
  callback(null, data);
});

Or maybe you can even simplify it to

s3.upload(AWSUploadObject, callback)

You also need to update your usage to a callback manner

uploadToAWSBucket(file, (error, image1) => {
  if (error) {
    next(error);
    return;
  }
  // your success code here
});

The final result is

const uploadToAWSBucket = (fileObject, callback) => {
  let randomFileName = shortid.generate();
  const AWSUploadObject = {
    Bucket: BUCKET_NAME,
    Key: randomFileName,
    Body: fileObject.buffer,
    ContentType: fileObject.mimetype,
  };


  s3.upload(AWSUploadObject, callback);
};

That's it. I hope this explanation will help you to understand how to use callbacks.

Pavlo Zhukov
  • 3,007
  • 3
  • 26
  • 43
  • If we are doing the inverse of classic promisification then `uploadToAWSBucket` should remain intact. We should write an adapter which calls `uploadToAWSBucket` and handles its success/error in such a way that a supplied callback is called. The original `uploadToAWSBucket` should not be destroyed and its internal workings need not be replicated. In fact, we needn't know anything about the internal workings of `uploadToAWSBucket`. – Roamer-1888 Jun 28 '20 at 23:42
  • @Roamer-1888, I haven't seen such a point in the question. I saw here a request of how to refactor the original function using callback approach. It's typical learning task to convert something from callback to Promise to async/await. I thought that author wants to rewrite `uploadToAWSBucket` implementation to callbacks. Let's see the author's feedback. – Pavlo Zhukov Jun 28 '20 at 23:52
  • Yes agreed, it's hard to predict the OP's expectation. – Roamer-1888 Jun 28 '20 at 23:54
  • 1
    Thanks @Pavlo Zhukov. This was helpful. – Neil S Jun 29 '20 at 15:12
1

If my understanding is correct, you want to use image1 after the catch block.

In that case, I suppose, you will be calling some function with image1. It can be done as follows, with some snippets taken from this answer:


const uploadToAWSBucket = (fileObject, callback) => { ... }; // described in the linked answer

uploadToAWSBucket(file, function callback(error, image1) {
  
  if(error) { return next(error); }

  someOtherFunction(image1, next); // "next" is passed as callback, with the assumption that nothing else needed to be called after that.  

});

If you want to call 2 more functions with the result of someOtherFunction, it can be done as follows:

uploadToAWSBucket(file, function callback(error, image1) {
  
  if(error) { return next(error); }

  someOtherFunction(image1, function someOtherFunctionCb(error, someOtherFunctionResult) {

    if(error) { return next(error); }

    someOtherFunction2(someOtherFunctionResult, function someOtherFunction2Cb(error, someOtherFunction2Result) {

      if(error) { return next(error); }

      someOtherFunction3(someOtherFunction2Result, function someOtherFunction3Cb(error, someOtherFunction3Result) {

        if(error) { return next(error); }

        next(null, someOtherFunction3Result);

      });

    }); 

  }); 

});

Basically, you cannot have local global variables if you use callbacks. I will try to explain a problem situation.


let image1 = null;

uploadToAWSBucket(file, function uploadToAWSBucketCallback(error, _image1) {
  
  if(error) { return next(error); }

  image1 = _image1;

});

someOtherFunction(image1, function someOtherFunctionCb(error, someOtherFunctionResult) {

  if(error) { return next(error); }

    ...

}); 

In the above snippet, someOtherFunction will be called before uploadToAWSBucketCallback is executed. That means, image1 is not assigned with _image1. Now, you know what will be the value of image1 when someOtherFunction is called.

The second snippet shows how to pass result of one async function to another, by nesting the subsequent calls inside the callbacks. This makes code less readable for many. There are libraries like async, which helps to make things easier & readable.

The second snippet can be rewritten with async library's waterfall function like this:

async.waterfall([
    function uploadToAWSBucketStep(callback) {
        uploadToAWSBucket(file, callback);
    },
    function someOtherFunctionStep(image1, callback) {
        someOtherFunction(image1, callback);
    },
    function someOtherFunction2Step(someOtherFunctionResult, callback) {
        someOtherFunction2(someOtherFunctionResult, callback);
    },
    function someOtherFunction3Step(someOtherFunction2Result, callback) {
        someOtherFunction3(someOtherFunction2Result, callback);
    }
], function lastStep(error, someOtherFunction3Result) {
    if(error) { return next(error); };
    next(null, someOtherFunction3Result);
});
explorer
  • 944
  • 8
  • 18
  • Clear answer and you anticipated one of my core followups (how to handle the situation where the result from the first callback is needed in another function call). @explorer – Neil S Jun 29 '20 at 15:16
0

Promisifcation of a callback-based function is well understood and well documented.

I have never seen a discussion of "de-promisification", but it is pretty simple.

Starting with your uploadToAWSBucket() and assuming you want your callback to be "nodeback" style (signature (err, data)), then you can write:

const uploadToAWSBucketNodeback = (fileObject, nodeback) => {
    uploadToAWSBucket(fileObject) // call the promise-returning "async" version.
    .then(data => { // success path
        nodeback(null, data);
    })
    .catch(nodeback); // error path
};

Or you could write a generic de-promisifier ...

const depromisify = (asyncFunction) => {
    return function(...params) {
        let nodeback = params.pop(); // strip nodeback off the end of params
        asyncFunction(...params)
        .then(data => { // success path
            nodeback(null, data);
        })
        .catch(nodeback); // error path
    }
};

... then

const uploadToAWSBucketNodeback = depromisify(uploadToAWSBucket);

Either approach will allow you to write:

uploadToAWSBucketNodeback(fileObject, function(err, data)) {
    if(err) {
        // handle error
    } else {
        // handle data
    }
}

Notes

  • we just need to know that the original asyncFunction is thenable/catchable.
  • the original asyncFunction is completely opaque to the depromisified function.
  • we don't need to know anything about the internal workings of the original asyncFunction. Thus, the composition of AWSUploadObject doesn't need to be replicated ..... it is still performed by uploadToAWSBucket().
Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
0

After reading everyone's responses, I came up with the following which I think works and is basically what @Pavlo Zhukov suggested. (Note the function names have changed slightly from my earlier post.)

Code from parent function:

let image1;
uploadToAWSBucketCallbackStyle(file, (err, data) => {
  if (err) {
    return next(err);
  }
  image1 = data;
  // Do stuff with image1 here or make additional function
  // calls using image1.
});


Child function:

const uploadToAWSBucketCallbackStyle = (fileObject, callback) => {
  let randomFileName = shortid.generate();
  const AWSUploadObject = {
    Bucket: BUCKET_NAME,
    Key: randomFileName,
    Body: fileObject.buffer,
    ContentType: fileObject.mimetype,
  };

  s3.upload(AWSUploadObject, callback);
}
Neil S
  • 41
  • 2