You are wondering what makes a function asynchronous. You may have heard that in a nutshell it's when argument of a function is a function. You're thinking:
"Does taking a callback make a function asynchronous?"
Well, basically yes, but there's a catch. Every asynchronous function takes a function argument, but not every function that does so is asynchronous. It matters how the argument is used inside the function.
Argument function can be called synchronously
Simply taking a callback doesn't make a function asynchronous. There are many examples of functions that take a function argument but are not asynchronous. For example there's forEach in Array. It iterates over each item and calls the function once per item.
This can be used among other things to calculate total value from a property of each item.
Calling the argument function is performed as part of normal step-by-step sequential execution of statements that make up the forEach implementation. Throwing an exception in the argument function would reveal a stack trace containing forEach and your function nested inside it.
It is very important to forEach to operate this way. If the argument function would be called asynchronously, code like in the example would not work as expected. The results would be zero each time.
Asynchronous function needs to perform an asynchronous operation
For a function to be asynchronous it needs to perform an asynchronous operation. It needs to incorporate the argument callback in handling the results of this asynchronous operation. Only this way the function becomes asynchronous.
For example a function reading contents of a file with the correct encoding can be implemented as an asynchronous function.
Your callback will not be called as part of the execution of readFileAsUtf8 function. It will be called only after the file contents have been read at some point in the future. It will be called asynchronously.
Throwing an exception from your callback would reveal a short stack trace originating from the event loop. It would not contain any mention of the function that originally took the function as an argument.
Ways to introduce asynchronicity
Asynchronous operation can be performed using the filesystem core module as seen in the example. Other ways of introducing asynchronicity can be
timer functions setTimeout, setInterval
special functions nextTick, setImmediate
listening to network, querying a database
reading or writing, generally I/O from a resource
subscribing to an event
Node.js doesn't wait for your database call to finish?
Learn the right mental model for Node apps. Get free email course and two chapters from Finish Your Node App book.