Zac Fukuda
072

On Writing Code - Fallback

One day in the winter of 2026 I was writing a Lambda function in TypeScript. I followed the “make it work, make it right, make it fast” principle. I had made my function work properly. It was time for me to to make my function right.

To make software right means to handle errors and exceptions right. A function that handles the error, so that the software would not crush or would recover in the case of crush, is called fallback.

It is impossible to create the fallbacks for all functions because you need a fallback for the fallback, then another fallback for that fallback, and so on. You fall into the fallback infinity .

Software engineers have to make a decision how much to implement fallbacks. Human resources are limited. They are always in short supply. Because all software developments reside with business, software engineers are encouraged to maximize their gross production with the minimum cost.

One fallback for One function

Whatever the cost, you have to at least create one fallback for each function. To do so, it is easier than ever with the catch block in the modern programming languages.

function errorProneFunctionA(): void {}
function errorProneFunctionB(): void {}
function errorProneFunctionC(): void {}

function main(): void {
  try {
    errorProneFunctionA();
    errorProneFunctionB();
    errorProneFunctionC();
  } catch (error: unknown) {
    // Fallback
  }
}

Your can write a fallback in a different way especially when the functions return promises.

function async errorProneFunctionA(): Promise<void> {}
function async errorProneFunctionB(): Promise<void> {}
function async errorProneFunctionC(): Promise<void> {}

function main(): void {
  errorProneFunctionA()
    .then(errorProneFunctionB)
    .then(errorProneFunctionC)
    .then(() => {})
    .catch((error: unknown) => {
      // Fallback
    });
}

I have seen many engineers who write try...catch in the main function, wrap all the statements, and gracefully catch all the errors like above. It is true that the try...catch in the main function can be the most effective and easiest approach in the web application where the system wants to respond the 500 HTTP status whenever the internal error occurs.

I, however, cannot embrace such a practice. Software engineers must handle each error of each function definitively.

async function main(): Promise<void> {
  try {
    await errorProneFunctionA();
  } catch (error: unknown) {
    // Fallback for A
  }

  try {
    await errorProneFunctionB();
  } catch (error: unknown) {
    // Fallback for B
  }

  try {
    await errorProneFunctionC();
  } catch (error: unknown) {
    // Fallback for C
  }
}

In early 2026 TypeScript engineers prefer try...catch to catch(). But you can make the code shorter with catch().

async function main(): Promise<void> {
  await errorProneFunctionA().catch((error: unknown) => {
    // Fallback for A
  });
  await errorProneFunctionB().catch((error: unknown) => {
    // Fallback for B
  });
  await errorProneFunctionC().catch((error: unknown) => {
    // Fallback for C
  });
}

Furthermore, you can predefine the fallbacks.

async function fallbackA(error: unknown): Promise<void> { … }
async function fallbackB(error: unknown): Promise<void> { … }
async function fallbackC(error: unknown): Promise<void> { … }

async function main(): Promise<void> {
  await errorProneFunctionA().catch(fallbackA);
  await errorProneFunctionB().catch(fallbackB);
  await errorProneFunctionC().catch(fallbackC);
}

Fallback for Fallback

The problems emerge when the fallbacks are prone to errors as well. The first draft of your function might look as follows.

async function getFoo(): Promise<Foo> {
  let foo: Foo | null = await getFooA();

  if (foo) return foo;

  foo = await getFooB();

  if (foo) return foo;
  
  foo = await getFooC();

  if (foo) return foo;
  else throw new Error();
}

However you implement fallbacks, you would end up Error! It is unlikely that the runtime would ever reach to the last else and throws an Error. So you let the global scope deal with the error.

The getFoo() would return the Foo 99.9% of the time. That means you manage to make your software right. Now you can release, can monitor, and can make the function faster if you find the any hindrance in the program. No, you cannot.

You could rewrite the function with the OR (||) operator.

async function getFoo(): Promise<Foo> {
  const foo: Foo | null = await getFooA() || await getFooB() || await getFooC();

  if (foo) return foo;
  else throw new Error();
}

Conditions confuse engineers. When you find them in the code, you try to come up with the other way around. Instead of returning null, the functions can throw errors when they are unable to get the Foos.

async function getFoo(): Promise<Foo> {
  try {
    return await getFooA();
  } catch {
    try {
      return await getFooB();
    } catch {
      return await getFooC();
    }
  }
}

Nesting try...catch over and over does not look good. You can replace try...catch with catch().

async function getFoo(): Promise<Foo> {
  return await getFooA().catch(async () => {
    return await getFooB().catch(async () => {
      return await getFooC();
    });
  });
}

The code still looks ugly. It reminds me of the callback hell in old JavaScript before the introduction of Promise. The Promise supports multiple catch() blocks.

async function getFoo(): Promise<Foo> {
  return await getFooA()
    .catch(async () => await getFooB())
    .catch(async () => await getFooC());
}

In fact, you can omit await keywords from the last three examples wherein either try...catch or catch() is used because the function returns Promise<Foo> after all as type hinted.

async function getFoo(): Promise<Foo> {
  return getFooA()
    .catch(() => getFooB())
    .catch(() => getFooC());
}

The modern TypeScript/JavaScript engineers may feel uncomfortable with an asynchronous function that returns Promise but that does not contain any await keyword.

If the functions getFooB() and getFooC() don’t have any arguments, you could pass function selves to the catch() blocks.

function getFoo(): Promise<Foo> {
  return getFooA()
    .catch(getFooB)
    .catch(getFooC);
}

This code might work but it would give other engineers an impression that the getFooB() is a fallback for the getFooA(), and the getFooC() is a fallback for the getFooB(). If that is the case, the last example has no problem. If the three functions are alternative ways to get a Foo, the second last example is preferable. Using one function because that would do the jobs at the aim of reducing the lines of code will never be a shortcut. Most of the times the action will lead to a mess.

Custom Errors

In addition to dedicating your time on engineering the fallbacks, there is another way to make your fallbacks right. That is to design your functions to throw errors in a way that the fallbacks for those functions would be simple.

You can achieve this by creating a custom Error subclass. The Error is called Exception in some programming languages.

class CustomError extends Error {
  public readonly customValue: CustomValue;

  constructor(message: string, customValue: CustomValue) {
    super(message);
    this.customValue = customValue;
  }
}

function errorProneFunction(value: SomeValue): void {
  if (!validate(value)) throw new CustomError();

  // …somewhere else
  if (someCondition()) throw new Error();
}

function fallback(error: unknown): void {
  if (error instanceof CustomError) { … }
  else if (error instanceof Error) { … }
  else { … }
}

function main(): void {
  const someValue = getSomeValue();
  errorProneFunction(someValue).catch(fallback);
}

The common custom errors that you’d like to create are such as MappingError, ValidationError. Ideally speaking the more custom errors are, the more robust your system is. In real world, however, too much custom errors are time consuming to design, to code, and to manage. You shall find the sweet spot.

Begin by creating no custom error. Create one if necessary as your software grows, as you find the common patterns that the sharable custom error can solve the problem.

You could throw anything other than Error class but you should refrain from doing so. Only throw Errors.

JSDoc Errors

As of March 2026 TypeScript does not yet have a native throws keyword. TypeScript engineers must rely on JSDoc to know the types of Error one function might throw. JSDoc has @throws tag.

/**
 * @throws {CustomError}
 * @throws {Error}
 */
function errorProneFunction(value: SomeValue): void { … }

TypeScript—either with tsc command or on your IDE— won’t tell you whether you are having mishandling the errors. Even with JSDoc you still have a chance to forget to handle the custom error. Nevertheless, at least your IDE can tell you the errors one function might throw by hovering your mouse on the function. This function reference feature helps you understand the function without looking at the source code, and improves the engineers’ productivity.

You should make the best effort to write the right fallbacks. So write @throws tags.