How to properly handle rejected promises in TypeScript


A long time ago, computing was almost entirely synchronous. You’d call a function, and it would return almost immediately. Then, as time went on, we started depending on resources that would take a little while to give a response. In the early days, we’d use callbacks for other functions to get called after our asynchronous function had completed, but this caused “callback hell”, where it would be hard to track what code was executing at a given time, and difficult to follow the logical progression of an app’s execution.

how to properly handle rejected promises in TypeScript

Fortunately, easier asynchronous operations were introduced to JavaScript and TypeScript in the form of promises. It may seem weird to use a word that conveys a sense of human emotion, ie: “I promise to buy you flowers”, but it certainly captures the temporal aspect. Making a promise is constructing a contract with the recipient that you will do something, but that thing is always in the future.

Promising someone that you’ll stand next to them and talk to them when you are standing next to them talking is uncanny at best, and slightly crazed at worst.

But sometimes, we need to break our promises. The flower shop shut early, or a remote API call didn’t work out as we hoped it would. Ah, a pity. But not the end of the world. Let’s see why it’s not.

Table of Contents

Learning to cope with rejection

Typically, when we fire off our asynchronous request, there are only a few possible outcomes.

  • It’s currently running (pending)
  • It’s completed successfully, or
  • It’s failed

When a promise resolves successfully, the expected program flow can continue. But when things go wrong, our promise gets rejected. This is obviously a problem because, if we don’t handle our error (or exception), the exception can “bubble up” the stack. If nothing is available to handle the rejection, then execution of our JavaScript will bail out.

Let’s see how we can handle promise rejection in a way that works for us.

Promise rejection in TypeScript (Angular)

We’ll use Angular to demonstrate how promise rejection works in TypeScript for client-side libraries. We only use Angular as a way to visualize what’s happening, so you should be able to apply these concepts to any TypeScript client-side app that you are working on.

To demonstrate, let’s create an array that has ten promises in it, and make every other promise fail:

export class NumbersComponent implements OnInit {

  promises = Array<Promise<any>>();

  ngOnInit(): void  number 

  willFail(): Promise<void>  async 

  randomAsyncNumber(): Promise<number> {
    return new Promise((resolve, reject) =>  number )
  }

And this is the HTML:

<div style="width: 100%; height: 100%; display: flex; flex-direction: column; gap:5px">
  @for (number of promises; track number) {
    <div style="min-height: 20px; border: 1px solid black;">
      <p>
        {{ number | async }}
      </p>
      <p>
        <i>Object Value: {{ number | json }}</i>
      </p>
    </div>
  }
</div>

We use the json directive so we can see what is happening inside the Promise as it executes. Every other Promise fails with a value of “error”, and no value given:

every other promise fails with a value of error and no value given

But what if we wanted to go back and re-run those failed promises? Sometimes we might have to, like if we have a list of items and some of them fail.

Promises execute and have a result; we can’t repeat them. So instead, we need to splice out the old failed promise, insert a new promise, and re-execute it. To achieve this, let’s make a new function that does just that:

fixPromise(index: number){
  const promise = this.randomAsyncNumber();
  this.promises.splice(index, 1, promise);
}

And then, within our for loop, add the button to retry the promise on failure:

@if ((number | json).indexOf("error") > -1){
  <button (click)="fixPromise($index)">Retry Promise</button>
}

The result is that the failed promises are taken out of the array, and a new functional promise is added instead:

gif of a failed promises are taken out of the array, and a new functional promise is added instead

Promises can specify types of the return. But you can’t specify a return type for the reject path. Instead, you can wrap your resolution in another object that indicates whether a request has succeeded or not.

Specifying a Result interface

To achieve this, lets create a new interface that describes this Result type object. It’s very simple: something to hold the result, and something to tell us whether the Promise has succeeded or not:

export interface Result{
  result: any;
  success: boolean;
}

Now, let’s make both of our functions resolve to this new type:

willFail(): Promise<Result> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({result: 'fail', success: false});
    }, 1000);
  });
}

randomAsyncNumber(): Promise<Result> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({
        result: Math.floor(Math.random() * 100),
        success: true
      });
    }, 1000)
  })
}

Both Promises resolve now, but they resolve to a typed object:

both Promises resolve now, but they resolve to a typed object

The benefit of this approach is that we can be sure that the Promise will always resolve to our expected type, as opposed to calling reject, which can only ever be any.

Uncaught errors in an observable chain

We can’t talk about Promises and asynchronous functionality in TypeScript without also mentioning Observables.

Promises that reject can cause our execution to stop, and errors within an Observable pipe can cause our logic to exit earlier than intended.

For example, this Observable will yield once with a valid result:

randomAsyncNumber(): Observable<Result> {
  return of(null).pipe(
    delay(1000),
    map(() => ({
      result: Math.floor(Math.random() * 100),
      success: true
    }))
  );
}

But what would this do?:

willThrowError(): Observable<Result> {
  return of(null).pipe(
    delay(1000),
    tap(() => {
      throw new Error('This observable has thrown an error!');
    }),
    map(() => ({ result: 'This should not be visible', success: false })),
    catchError(error => {
      // This will not prevent the observable from entering error state
      // It just transforms the error into a Result object
      return of({ result: error.message, success: false });
    })
  );
}

Normally, we’d expect each operator within our pipe to be evaluated. But within the tap, an error is thrown. This causes the catchError operator to run before the Observable completes.

Catching errors within our Observable pipe is always a good idea because if our Observable errors, then it will continue to process events, as opposed to just ditching out.

Conclusion

Things don’t always work out, and when they don’t, that’s okay. Catching exceptions within our Promises and Observables with some good type information can help us degrade gracefully and retry certain operations when it makes sense.

How do you handle asynchronous errors in your applications? Let us know in the comments below. And don’t forget to check out our code samples from this article here.

The post How to properly handle rejected promises in TypeScript appeared first on LogRocket Blog.


Share this content:

I am a passionate blogger with extensive experience in web design. As a seasoned YouTube SEO expert, I have helped numerous creators optimize their content for maximum visibility.

Leave a Comment