Returning Values From Async Functions

An async function always returns a Promise, never the value written after return. Getting whereAmI's result out means consuming that Promise, and rethrowing an error inside it is what makes .catch() actually fire.

July 12, 20267 min read19 / 22

The last post wrapped whereAmI in try/catch, but the function still only logs its result, it never hands that result back to whoever called it. Making that work exposes something about async that isn't obvious from the outside.

Proving an Async Function Runs in the Background

Two console.log calls, one right before calling whereAmI() and one right after, settle the order question immediately.

JavaScript · Live Editor
Loading editor...

Hit Run. The order is 1, 3, then Finished getting location, never 1, Finished getting location, 3. Calling an async function doesn't pause the script waiting for it, it starts the function, hands back control immediately, and the rest of the file keeps running while the await inside resolves later in the background. A regular function with a console.log in the same spot would land between 1 and 3. This one can't, because by the time line 3 runs, whereAmI hasn't gotten anywhere near its await yet.

What return Actually Does Inside an Async Function

whereAmI already builds a string worth returning, You are in {city}, {country}. The natural instinct is to treat the call like a regular function and store the result directly.

JavaScript · Live Editor
Loading editor...

Hit Run. What prints is a Promise object, not the string. At the moment console.log(result) runs, whereAmI is still executing, await hasn't settled yet, and JavaScript has no way to hand over a value that doesn't exist yet without also blocking the whole script until it does. So it hands over the only thing it can: a pending Promise.

That Promise isn't empty for long. Whatever an async function returns becomes the fulfilled value of the Promise it returns. The string above is exactly what that Promise settles to, so getting it out means consuming the Promise the same way any other one gets consumed, .then():

The lifecycle of a Promise: pending while the task runs, then settling exactly once into either fulfilled or rejected, a state that never changes afterward. ExpandThe lifecycle of a Promise: pending while the task runs, then settling exactly once into either fulfilled or rejected, a state that never changes afterward.

JavaScript · Live Editor
Loading editor...

Hit Run and wait for it. You are in Lisbon, Portugal prints, pulled out of the returned Promise through .then(), the exact same mechanic that first turned a raw Promise into a usable value back in post 7.

A Caught Error Still Fulfills the Promise

Swap the resolved data for something that throws, and the mismatch shows up immediately.

JavaScript · Live Editor
Loading editor...

Hit Run. dataGeo is null, reading .city off it throws, the catch block logs the error exactly as designed, and then .then() still fires, logging undefined. The catch block swallowed the error and returned nothing, and an async function that returns nothing still returns a Promise, one that's fulfilled with undefined. A caught error doesn't reject the Promise an async function returns, it only stops the error from crashing anything. Adding a .catch() after the .then() here would never run either, the Promise never rejected in the first place.

Rethrowing Turns the Catch Back Into a Rejection

Fixing that means putting the error back onto the Promise on purpose, by throwing again from inside the catch block.

JavaScript · Live Editor
Loading editor...

Hit Run. Both error logs appear now, the one inside catch and the one chained after .then(). Rethrowing from inside a try/catch is what manually rejects the Promise an async function returns, the same way throwing inside a .then() manually rejected a Promise chain back in post 10. Without the rethrow, the catch block absorbs the error completely and the returned Promise settles as fulfilled regardless.

A Plain Log Still Beats the Async Result

One more ordering trap, easy to miss once error handling is in the mix. A log written right after calling whereAmI(), the same way 3: Standalone log worked back in the first example, still fires before the .catch() below it ever gets a chance to run.

JavaScript · Live Editor
Loading editor...

Hit Run. The order is 1, 3, then both 2s, not 1, 2, 3 like the numbers suggest it should read. whereAmI() is still called synchronously, so control returns immediately and 3 runs before the rejection has even happened yet. A log meant to run "after everything's done" has to be attached to the chain itself, not written as a plain line after the call, and .finally() is built exactly for that:

JavaScript · Live Editor
Loading editor...

Now 3 only logs once the whole chain has actually settled, success or failure, giving the real 1, 2, 3 order. The async IIFE version below gets this for free: a line written after the try/catch block, still inside the same async function, naturally waits for the await above it to finish first.

An Async IIFE Instead of Mixing Styles

Chaining .then()/.catch() off an async function call works, but it's mixing two philosophies in one place, the whole point of async/await was to stop writing .then() at all. await only works inside an async function, though, and whereAmI is already one, so calling it doesn't need a new named function, just an immediately-invoked one:

JavaScript · Live Editor
Loading editor...

Hit Run, same 1, 2, 3 order as the .finally() version, no .then() or .catch() anywhere in sight. 3 sits after the try/catch but still inside the async function, so it only runs once the await above it has actually settled, the same guarantee .finally() gave the chained version. An async IIFE is one of the few spots the IIFE pattern still earns its keep, it's the only way to get an await at the top level of a script without a name attached to the function wrapping it.

The Essentials

  1. Calling an async function never pauses the caller. Code after the call keeps running while the function's own await resolves in the background.
  2. An async function always returns a Promise, and whatever it returns becomes that Promise's fulfilled value, gotten out through .then() or a second await.
  3. A catch block that handles an error without rethrowing it leaves the async function's Promise fulfilled, not rejected, usually with undefined.
  4. Rethrowing inside catch is what turns a handled error back into a real rejection that .catch() further down the chain will actually see.
  5. A plain log written right after an async call still runs before that call settles, .finally() (or a line placed after await inside an async function) is what actually waits for it.
  6. An async IIFE is how await runs without a named async function wrapping it, and it keeps a whole call site on async/await instead of mixing in .then().