asynchronous programming
function design
JavaScript
programming best practices
software development

Why shouldn't all functions be async by default?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Async functions are valuable when a function genuinely needs to wait for asynchronous work such as network I/O, timers, or file access. They should not be the default for every function, because marking everything async changes return types, adds promise machinery, and makes otherwise simple synchronous code harder to reason about.

async Changes the Contract

The most important effect of async is not syntax. It is the function contract. An async function always returns a promise, even when the body computes a value immediately.

javascript
1function addSync(a, b) {
2  return a + b;
3}
4
5async function addAsync(a, b) {
6  return a + b;
7}
8
9console.log(addSync(2, 3));
10console.log(addAsync(2, 3));

addSync returns 5. addAsync returns a promise that eventually resolves to 5. That difference propagates outward into every caller.

Unnecessary Async Spreads Complexity

Once a function becomes async, callers often need await or promise handling even when the work itself was purely synchronous. That can create a chain reaction through an otherwise simple code path.

javascript
1function formatName(user) {
2  return `${user.first} ${user.last}`;
3}
4
5async function getDisplayName(user) {
6  return formatName(user);
7}

In that example, getDisplayName gained no real asynchronous behavior, but every caller now has to treat it as asynchronous anyway.

Async Is for Real Waiting

Use async when the function coordinates operations that do not complete immediately:

javascript
1async function loadUser(userId) {
2  const response = await fetch(`https://example.com/users/${userId}`);
3  return response.json();
4}

This is exactly what async is for. It makes asynchronous control flow easier to read while preserving the real promise-based behavior that the work already requires.

Synchronous Code Still Has Value

Pure computation, validation, formatting, and small transformations are often clearer as synchronous functions. They are easier to test, easier to compose, and easier to call from either synchronous or asynchronous code.

A good design pattern is:

  • keep pure logic synchronous
  • make boundary functions async when they talk to I/O
  • let async code call sync helpers, not the other way around unless necessary

That keeps the promise boundary close to the actual asynchronous work.

Async Also Affects Error Handling and Scheduling

An async function changes how errors surface and when results become available. Exceptions become rejected promises, and the result is observed asynchronously. That is correct when the function is truly asynchronous, but it is unnecessary cognitive overhead for plain CPU-bound helpers.

Async and Sync Composition Should Be Intentional

Async code can always call synchronous helpers directly, which is one reason keeping small logic functions synchronous is so useful. The opposite direction is more awkward because synchronous callers must now adopt promise handling just to use a function that never really needed asynchronous behavior.

In hot paths or utility-heavy code, turning everything async can also complicate performance profiling and control flow without giving any benefit.

Common Pitfalls

  • Marking helper functions async just because they are called from async code.
  • Forgetting that async changes the return type to a promise.
  • Forcing callers to add await for work that was originally synchronous and immediate.
  • Mixing real I/O functions with pure transformation functions so the async boundary spreads farther than needed.
  • Assuming async automatically makes code faster. It helps structure asynchronous work; it does not eliminate the underlying waiting cost.

Summary

  • Functions should be async when they actually coordinate asynchronous work.
  • 'async changes the API contract by returning a promise.'
  • Making everything async spreads complexity to callers for no benefit.
  • Keep pure computation and validation synchronous when possible.
  • Put the async boundary near real I/O instead of making it the default everywhere.

Course illustration
Course illustration

All Rights Reserved.