An async function always returns a promise. That's how it reports the completion of its asynchronous work. If you're using it in another async function, you can use await to wait for its promise to settle, but in a non-async function (often at the top level or in an event handler), you have to use the promise directly, e.g.:
latestTime()
.then(time => {
    console.log(time);
})
.catch(error => {
    // Handle/report error
});
...though if you're doing this at the top level of a JavaScript module, all modern environments now support top-level await in modules:
const time = await latestTime();
(Note that if that promise is rejected, your module will fail to load. If your module can work meaningfully even if the promise fails, be sure to wrap that in try/catch to handle promise rejection.)
It might (or might not) throw some light on things to see, in explicit promise callback terms, how the JavaScript engine handles your async function under the covers:
function latestTime() {
    return new Promise((resolve, reject) => {
        web3.eth.getBlock('latest')
        .then(bl => {
            console.log(bl.timestamp);
            console.log(typeof bl.timestamp.then == 'function');
            resolve(bl.timestamp);
        })
        .catch(reject);
    });
}
Some important notes on that:
- The function you pass to new Promise(the promise executor function) gets called synchronously bynew Promise.
- Which is why the operation starts, web3.eth.getBlockis called synchronously to start the work.
 
- Any error (etc.) thrown within the promise executor gets caught by new Promiseand converted into a promise rejection.
- Any error (etc.) thrown within a promise callback (like the one we're passing then) will get caught and converted into a rejection.