Asynchronous programming is
crucial for modern applications, but managing multiple callbacks and events can quickly turn into a tangled mess—commonly known as "callback hell".
But the very need for
asynchronous calls is to avoid GUI thread freezes when for example HTTP
requests take an unpredictable amount of time. To tackle this challenge, we are excited to introduce
beta support for promises in
TMS FNC Core! With promises, you can write
clean, sequential code for cloud-based operations while maintaining all the benefits of asynchronous behavior.
The Challenge
TMS FNC Cloud Pack relies on asynchronous requests to communicate with various cloud services, such as Google, PayPal,
Stellards.io, and more. If you've worked with our components, you’re familiar with how asynchronous communication is handled via callbacks and events.
While effective, this approach can become difficult to manage. For example, to view an event from the Google Calendar API, you need to go through all these steps:
- Authenticate the user.
- Retrieve a list of available calendars.
- Fetch the events for a specific calendar.
Using callbacks and events for such workflows can quickly spiral into complexity, making the code harder to read, maintain and debug.
Promising Solution
Being so used to using promises in the context of web applications, where by design, several functions work asynchronously, we missed a promises implementation for a long time for native Delphi code. Especially in the area of using REST API functions, the ability to use promises significantly simplifies writing code that needs to be built with chained asynchronous calls.
It was with much enthusiasm that we discovered the promises library written by Laurens van Run from MendriX earlier this year. This triggered us to get in touch with Laurens and see how we could both spread the message about this fantastic new Delphi promises library and also leverage this in the context of our components, especially the TMS FNC Cloud Pack that is built on top of asynchronous REST APIs.
By working together with Laurens, we also contributed to make his promises library cross-platform and offer it integrated as part of TMS FNC Core to make using it from TMS FNC Cloud Pack as seamless as possible.
We thank Laurens so much for all his efforts that went into this. Laurens did a great job for the Delphi community and his promises library is not the only one. Laurens also actively contributes to projects like DelphiCodeCoverage and Delphi-Mocks. And Laurens also revitalized the (old) Delphi SonarQube plugin and worked with Embarcadero to bring attention to it.
Together with his colleagues at Mendrix and the Delphi community, Laurens drives forward innovation in the Delphi world. Check also what jobs Mendrix has to offer for Delphi developers here: https://werkenbijmendrix.nl/
Example
Let’s break down a small example to understand the
building blocks of using promises. In this example, we’ll request the bordering countries of a given country using the
REST Countries API. The API returns an array of country codes for the bordering nations, so we’ll need to make additional requests to retrieve their full names.
1. Deferred Promises
The core mechanism relies on deferred promises, which are created using
Promise.New. This method takes an executor function as a parameter. The executor function provides two parameters,
AResolve and
AReject, which allow you to resolve or reject a promise after an asynchronous operation completes. These parameters can be persisted and invoked later or used in a callback directly:
-
-
-
-
- function TForm1.ExecuteRestCountryRequest(APath: string; ACountryOrCode: string): IPromise<string>;
- begin
- Result := Promise.New<string>(procedure (AResolve: TProc<string>; AReject: TProc<Exception>)
- begin
- c.Request.Clear;
- c.Request.Host := 'https://restcountries.com/v3.1';
- c.Request.Method := rmGET;
- c.Request.Path := APath + '/' + ACountryOrCode;
- c.Request.ResultType := rrtString;
- c.ExecuteRequest(procedure (const ARequestResult: TTMSFNCCloudBaseRequestResult)
- begin
- if ARequestResult.Success then
- AResolve(ARequestResult.ResultString)
- else
- AReject(Exception.Create('Request failed. No country or code found: ' + ACountryOrCode));
- end);
- end);
- end;
//We will query two endpoints:
//https://restcountries.com/v3.1/name/{CountryName}
//https://restcountries.com/v3.1/alpha/{CountryCode}
//Create a common request method that we can reuse
function TForm1.ExecuteRestCountryRequest(APath: string; ACountryOrCode: string): IPromise<string>;
begin
Result := Promise.New<string>(procedure (AResolve: TProc<string>; AReject: TProc<Exception>)
begin
c.Request.Clear;
c.Request.Host := 'https://restcountries.com/v3.1';
c.Request.Method := rmGET;
c.Request.Path := APath + '/' + ACountryOrCode;
c.Request.ResultType := rrtString;
c.ExecuteRequest(procedure (const ARequestResult: TTMSFNCCloudBaseRequestResult)
begin
if ARequestResult.Success then
AResolve(ARequestResult.ResultString)
else
AReject(Exception.Create('Request failed. No country or code found: ' + ACountryOrCode));
end);
end);
end;
2. Chaining Chaining ensures sequential execution of operations while preserving asynchronous behavior. Each
.ThenBy call returns a new
IPromise<T> and defines what happens after the current promise resolves. The value returned from each step is passed to the next in the chain, reducing the need for nested callbacks.
A simple example on chaining:
- function TForm1.GetCountryNameFromCode(ACode: string): IPromise<string>;
- begin
- Result := ExecuteRestCountryRequest('/alpha', ACode)
- .ThenBy(function (const AValue: string): string
- begin
-
- Result := GetCountryNameFromJSON(value);
- end);
- end;
function TForm1.GetCountryNameFromCode(ACode: string): IPromise<string>;
begin
Result := ExecuteRestCountryRequest('/alpha', ACode)
.ThenBy(function (const AValue: string): string
begin
//AValue contains the JSON response, parse it to get the name:
Result := GetCountryNameFromJSON(value);
end);
end;
In the snippet above, ExecuteRestCountryRequest is used to make an API call, and the response is parsed in a chained step to extract the desired data—in this case, the country name.
3. Chain multiple deferred promises
Understanding this step is important before moving on to the final step of waiting for multiple deferred promises to complete.
It might seem a bit complex at first glance, but combining multiple deferred promises with chaining is actually simple. The key to understanding this is that a promise is resolved when the Result is set. This means we need to return a promise from the .ThenBy method, ensuring that the chain continues only after the current promise has been resolved.
Here’s how it works in practice:
- Promise.New<TVoid>(procedure(AResolve: TProc<TVoid>; AReject: TProc<Exception>)
- begin
-
- end)
- .ThenBy(function(const AResult: TVoid): IPromise<TVoid>
- begin
- Result := Promise.New<TVoid>(procedure(AResolve: TProc<TVoid>; AReject: TProc<Exception>)
- begin
-
- end);
- end)
- .ThenBy(function(const AResult: TVoid): IPromise<TVoid>
- begin
- Result := Promise.New<TVoid>(procedure(AResolve: TProc<TVoid>; AReject: TProc<Exception>)
- begin
-
- end);
- end);
Promise.New<TVoid>(procedure(AResolve: TProc<TVoid>; AReject: TProc<Exception>)
begin
//Do the first promisified call
end)
.ThenBy(function(const AResult: TVoid): IPromise<TVoid>
begin
Result := Promise.New<TVoid>(procedure(AResolve: TProc<TVoid>; AReject: TProc<Exception>)
begin
//Do the second promisified call
end);
end)
.ThenBy(function(const AResult: TVoid): IPromise<TVoid>
begin
Result := Promise.New<TVoid>(procedure(AResolve: TProc<TVoid>; AReject: TProc<Exception>)
begin
//Do the third promisified call
end);
end);
4. Wait for multiple promises
Promise.All allows us to wait for multiple promises to resolve, and we can apply the
same logic as before. The key difference is that, instead of returning a single IPromise<T>, we now return an array of them:
- procedure TForm1.GetBorderingCountires(ACountry: string);
- begin
-
- ExecuteRestCountryRequest('/name', ACountry)
- .Op.ThenBy<TArray<string>>(function (const AValue: string): IPromise<TArray<string>>
- var
- LBorders: TArray<string>;
- LPromises: TArray<IPromise<string>>;
- I, LBorderCount: Integer;
- begin
-
-
- LBorders := GetBorderingCountriesFromJSON(AValue);
- LBorderCount := Length(LBorders);
- SetLength(LPromises, LBorderCount);
-
-
-
- for I := 0 to LBorderCount - 1 do
- LPromises[I] := GetCountryNameFromCode(LBorders[I]);
-
-
- Result := Promise.All<string>(LPromises);
- end)
- .Main.ThenBy<TVoid>(function (const AValues: TArray<string>): TVoid
- var
- I: Integer;
- begin
-
- if FFormNotDestroyed then
- begin
- for I := 0 to Length(AValues) - 1 do
- Memo1.Lines.Add(AValues[I]);
- end;
-
- Result := Void;
- end)
- .Main.Catch(procedure (E: Exception)
- begin
-
- ShowMessage(E.Message);
- end);
- end;
procedure TForm1.GetBorderingCountires(ACountry: string);
begin
//Start by getting the country name
ExecuteRestCountryRequest('/name', ACountry)
.Op.ThenBy<TArray<string>>(function (const AValue: string): IPromise<TArray<string>>
var
LBorders: TArray<string>;
LPromises: TArray<IPromise<string>>;
I, LBorderCount: Integer;
begin
//Parse the returned JSON to retrieve the list of
//bordering countries
LBorders := GetBorderingCountriesFromJSON(AValue);
LBorderCount := Length(LBorders);
SetLength(LPromises, LBorderCount);
//Create a promise for each country code, these are individual
//requests:
for I := 0 to LBorderCount - 1 do
LPromises[I] := GetCountryNameFromCode(LBorders[I]);
//Wait for all the promises to complete
Result := Promise.All<string>(LPromises);
end)
.Main.ThenBy<TVoid>(function (const AValues: TArray<string>): TVoid
var
I: Integer;
begin
//Cautiously update the UI reflecting the list:
if FFormNotDestroyed then
begin
for I := 0 to Length(AValues) - 1 do
Memo1.Lines.Add(AValues[I]);
end;
Result := Void;
end)
.Main.Catch(procedure (E: Exception)
begin
//Show errors - if any:
ShowMessage(E.Message);
end);
end;
And that’s it! By calling GetBorderingCountries from a button click, you’ll fetch the list of neighboring countries while keeping the UI responsive.
Get Started and Share Your Feedback
If you'd like to see something more practical, particularly in combination with our TMS FNC Cloud Pack components, take a look at the TTMSFNCCloudGoogleCalendar demo we’ve prepared, available under the Demo\Promises folder! It not only demonstrates the concepts discussed above but also shows how to keep your codebase cleaner by inheriting from TTMSFNCCloudGoogleCalendar and handling promises internally.
While
awaiting that Christmas dinner to bake in the oven, take a moment to explore how promises can simplify your workflows and make your code more maintainable. The implementation of promises is now available for registered users, integrated into TMS FNC Core as a
BETA! To make the most of the Delphi-Promises library, be sure to
check out the documentation, which offers detailed guidance to help you get started quickly.
The integration requires Delphi 10.2 or later, so ensure your development environment meets the minimum requirements.
Your feedback will play a valuable role in shaping the future of promises within our FNC range. Dive into the BETA, try it out, and let us know how promises are impacting your development experience!