Skip to main content

Migrating from Promise

Promise is the de-facto API to work with async data. Observables don't replace them, but they cover some specific limits of the Promises.

We will discuss of these particular use-cases in this tutorial.

Difference between a Promise and an Observable

First, we need to understand the differences between a Promise and an Observable:

  • A Promise sends only one value, where an Observable may emit many values.
  • A Promise cannot be cancelled (or with special tricks like AbortSignal). But, Observables are based on a subscribe/unsubscribe mechanism, so cancellation is native for them.
  • A Promise is executed immediately, whereas with Observables, we first define the pipeline, and then, by subscribing later, we run this pipeline.

So if we're facing one of these limits, then an Observable is the perfect candidate.

How to represent a Promise with an Observable ?

A Promise has an internal state:

  • pending: the Promise is not resolved yet
  • fulfilled: the Promise is resolved without error and has a value
  • rejected: the Promise is rejected with an error

However, an Observable may emit any kind of value. In order to mimic the behaviour of a Promise we will use some Notifications:

To represent a fulfilled Promise:

  • we have to emit a next Notification
  • followed by a complete one

And to represent a rejected Promise:

  • we have to emit an error Notification

It's a little trick. We will see now some examples.

Observable equivalent of the Promise's methods

Promise.resolve

As said earlier, a fulfilled Promise, may be represented by a next Notification followed by a complete one.

This function has the name of singleWithNotifications.

It simply does:

function singleWithNotifications<GValue>(
value: GValue,
): IObservable<ISingleObservableNotifications<GValue>> {
return (emit: IObserver<ISingleObservableNotifications<GValue>>): IUnsubscribe => {
emit(createNextNotification<GValue>(value));
emit(STATIC_COMPLETE_NOTIFICATION);
return noop;
};
}

Promise.reject

On another hand, a rejected Promise, may be represented by an error Notification.

This time, the function has the name of throwError.

And it does:

function throwError<GError>(
error: GError,
): IObservable<IErrorNotification<GError>> {
return (emit: IObserver<IErrorNotification<GError>>): IUnsubscribe => {
emit(createErrorNotification<GError>(error));
return noop;
};
}

Promise.all

Promise.all is used to await all promises to fulfill from a list of promises. If one is rejected, it will reject too.

The Observable's equivalent is forkJoin or allWithNotifications.

Promise.race

Promise.race is used to await the first promise to resolve (fulfilled or rejected) from a list of promises.

The Observable's function is: raceWithNotifications.

Promise.any

Promise.any is used to await that one promise fulfills from a list of promises. If all the promises reject, it will reject too.

With Observables: anyWithNotifications.

Chaining -> then / catch

A key point of Promises is they capability to be chained using the .then and .catch methods.

then

The .then method of a Promise accepts two optional/nullable arguments:

  • an onFulfilled function (called when the Promise is fulfilled)
  • and an onRejected function (called when the Promise is rejected)

However, the Observables are stricter, and provides distinct functions, for each case:

catch

So the .catch method becomes rejectedObservablePipe, with Observables.

finally

Lastly, the .finally method is used to handle fulfilled or rejected states.

With @lirx/core, it's called finallyObservablePipe.

Casting an Observable to a Promise

In most cases, we simply want the last received value (last next Notification), so we can use the function toPromiseLast. However, sometimes, we want to get all the values as an array. In this case, we can use the function toPromiseAll. .

caution

toPromise MUST only be used with Observables NOT sending notifications.

Example

function noCORS(url: string): string {
const _url: URL = new URL(`https://cors-anywhere.herokuapp.com/`);
_url.pathname = url;
return _url.href;
}

interface IGeoJSGetGeoJSON {
organization_name: string;
region: string;
accuracy: number;
asn: number;
organization: string;
timezone: string;
longitude: string;
country_code3: string;
area_code: string;
ip: string;
city: string;
country: string;
continent_code: string;
country_code: string;
latitude: string;
}

// 1) prepare the request pipeline
const request$ = pipe$$(fromFetch(noCORS(`https://get.geojs.io/v1/ip/geo.json`)), [
fulfilled$$$((response: Response): IObservable<IDefaultNotificationsUnion<IGeoJSGetGeoJSON>> => {
if (response.ok) {
return fromPromiseFactory(() => response.json());
} else {
return throwError(createNetworkError());
}
}),
fulfilled$$$((data: IGeoJSGetGeoJSON): IObservable<IDefaultNotificationsUnion<string>> => {
return singleN<string>(data.country);
}),
]);

const doRequestOnClick$ = pipe$$(fromEventTarget(window, 'click'), [
switchMap$$$(() => request$),
]);

doRequestOnClick$((notification: IDefaultNotificationsUnion<string>) => {
console.log(notification.name, notification.value);
});

Output:

// user clicks
'next', United States
'complete', undefined

Click here to see the live demo