Enhancements to 1C:Enterprise language syntax for asynchronous functions: synchronous asynchrony

This article is an announcement of new functionality.
We do not recommend that you use this article to learn the new functionality.
A full description of the new functionality will be provided in the documentation for the appropriate version.
For a complete list of changes in the new version, see the v8Update.htm file.

Implemented in version 8.3.18.1128

Some time after the web client appeared in 1C:Enterprise platform, it became necessary to support the asynchronous model, which at that time appeared in browsers. This support was implemented using the NotifyDescription type and new asynchronous procedures.

In version 8.3.18, changes were made to make it easier to work with asynchrony. It is based on the idea that it is easier and clearer for a developer to see the code that consists of a set of sequentially performed operations and contains a minimum number of details not directly related to the task they are solving. And the changes made are aimed at making the code that deals with asynchrony as similar as possible to a regular sequence code.

The closest analog of what has been added in terms of asynchrony in 1C:Enterprise is the Promise type and the async/await statements of the JavaScript language introduced in the ECMAScript 6 and ECMASript 2017 standards, respectively.

Asynchronous methods are still available only where they were available before. The very essence of asynchrony, as it is already implemented in 1C:Enterprise, does not change. This is a new form of working with asynchrony.

Next, we'll look at the components of the new mechanism, as well as some features and common practices.

Components

We can say that the new form of working with asynchrony includes three components:

  • Promise type
  • "New" asynchronous platform functions that return Promise
  • Async and Await statements in 1C:Enterprise language

Let's take a look at these components.

Promise type

This type is a central part around which the rest of the components are grouped. In short, Promise is a wrapper for the possibly unknown result of asynchronous function execution.

An asynchronous function, like any other function, can have two completion options:

  • Normal completion
  • Exception

Accordingly, the Promise object can be in one of three states:

  • Pending. The asynchronous function has not completed yet and the result has not been determined yet. This is the initial state for Promise objects.
  • Success. The function is completed successfully. As a result, Promise becomes a container of the value returned by the asynchronous function.
  • Failure. An uncaught exception was thrown while executing the asynchronous function. Promise becomes a container for this exception.

Almost any Promise starts in the Pending state. Then, depending on how the asynchronous function is completed, Promise can enter the Success or Failure state. And if Promise enters the Success or Failure state, it will remain in this state until the end of its life.

Promise is the return value of the "new" asynchronous functions. Typically, an asynchronous function returns Promise even before it is actually completed. That is, this Promise is in the Pending state.

At the moment, Promise has no constructors, methods, or properties available from 1C:Enterprise language. That is, the new Promise object can only appear as a result of calling an asynchronous function. All you can do with the Promise object is use it as an argument of the Await operator.

For the rest, Promise is a quite common type. We can use a value of the Promise type to do the same things as with any other value: assign it to variables, pass it as a parameter value to procedures and functions, and so on.

"New" asynchronous platform functions

To take advantage of the new approach to working with asynchrony, almost all existing asynchronous procedures of the platform version 8.3.18 have analogs in the form of asynchronous functions that return Promise.

In what follows, the previously existing asynchronous procedures will be referred to as the "old" asynchronous procedures and the added asynchronous functions that return Promise will be called the "new" asynchronous functions.

The main difference between the "new" asynchronous functions and the "old" asynchronous procedures is the way used to organize code execution after actual completion of the asynchronous operation. To do this, the "old" asynchronous procedures used the <NotifyDescription> parameter in which the caller had to place the NotifyDescription object containing a description of the procedures to be called if the asynchronous operation succeeded or if an error occurred.

The "new" asynchronous platform functions do not have the <NotifyDescription>, parameter, but instead they return Promise that wraps the result of asynchronous function execution. The way the caller deals with this return value is entirely up to the caller.

The "old" asynchronous procedures often had prototypes in the form of synchronous procedures or functions based on which they were created.

Let's look at the example below.

1C:Enterprise 8 platform has the synchronous CopyFile function, whose description is as follows:

CopyFile(<SourceFileName>, <DestinationFileName>)

Parameters:

  • <SourceFileName> (required)
    Type: String
    Full source file name
  • <DestinationFileName> (required)
    Type: String
    Full source file name

This method is available everywhere. Using synchronous methods on the client may be prohibited. Starting from version 8.3.6, the platform has the asynchronous  BeginCopyingFile procedure with the following description:

BeginCopyingFile(<NotifyDescription>, <SourceFileName>, <DestinationFileName>)

Parameters:

  • <NotifyDescription> (required)
    Type: NotifyDescription
    Contains a description of the procedure that will be called upon completion with the following parameters:

    • <CopiedFile> is a string that contains the path to the copied file
    • <AdditionalParameters> is a value that was specified when the NotifyDescription object was created
  • <SourceFileName> (required)
    Type: String
    Full source file name
  • <DestinationFileName> (required)
    Type:String
    Full source file name

And finally, the "new" asynchronous CopyFileAsync function is available starting from version 8.3.18:

CopyFileAsync(<SourceFileName>, <DestinationFileName>)

Parameters:

  • <SourceFileName> (required)
    Type: String
    Full source file name
  • <DestinationFileName> (required)
    Type:String
    Full source file name

Returns:

  • Type: Promise

After the file is copied successfully, Promise will contain the path to the copied file. In case of error, Promise will contain an exception.

The example above shows that in some aspects, the "new" asynchronous function has more similarities with the synchronous analog than with the "old" asynchronous procedure. In particular, the "new" asynchronous function has the same parameter list as the synchronous procedure. And the name of the new asynchronous procedure is formed by simply adding the Async suffix to the name of the synchronous procedure.

It is asynchronous execution that the "new" asynchronous function has in common with the "old" asynchronous procedure. Also the similarity lies in that Promise, on successful completion, receives the same value as the first parameter of the procedure represented by the NotifyDescription object.

For some "old" asynchronous procedures, NotifyDescription must contain a description of the procedure with a single <AdditionalParameters> parameter. In this case, for the added "new" asynchronous function, Promise will always contain Undefined upon successful completion.

Async and Await

And finally, the top of the whole construction is Async and Await.

Async is a modifier that can be applied to a procedure or function written in 1C:Enterprise language. Using this modifier makes a procedure or function asynchronous. Another important feature is that the Await operator can only be used inside Async procedures and functions.

The Await operator argument is Promise. Logically, the Await operator waits for the asynchronous function that follows the Promise object to complete. Upon normal function completion, the operator result is the value returned by the asynchronous function. If an exception is thrown inside the function, the same exception is thrown from the Await operator.

Thus, executing

Res = Await Pr; // Pr has the Promise type

can have one of the following results:

  • The Res variable will be assigned a value that was wrapped in Promise as a result of normal completion of the asynchronous function.
  • The Await operator will throw an exception that was thrown inside the asynchronous function and got wrapped in Promise.

Let's consider some features of Async procedures and functions.

Passing parameters by value

All parameters of Async procedures and functions are passed by value only. In ordinary procedures and functions, to pass a parameter by value, the Val keyword must appear before its name. In Asynс procedures and functions, passing by value is carried out by default and is the only possible option. It does not matter whether the Val keyword is actually present.

So, the function headers

Async Function CopyFilesAsync(SourceDirectory, TargetDirectory)

and

Async Function CopyFilesAsync(Val SourceDirectory, Val TargetDirectory)

are equivalent.

Async Function always returns Promise

Async function always returns Promise. Upon successful completion, Promise will wrap the value that was the Return operator argument. If an exception is thrown while executing the Async function, this exception will be wrapped in Promise.

An exception thrown and not caught inside the Async function cannot get into the caller as a result of calling the function.

Procedure P()
    Try
        Func1(); 
    Except
    // Exception from Func1() will not be caught here
    EndTry
EndProcedure

Async Function Func1()
    Raise "Thrown into Func1()";
EndFunction

The only way to know how the Async function is completed is to use Promise returned by it as an argument of the Await operator.

Async Procedure P()
    Try
        Await NotNull(Null); 
    Except
    // Exceprion from NotNull() will be caught here
        Report("Null passed"); 
    EndTry
EndProcedure

Async Function NotNull(P)
    If P = Null Then
        Raise "Thrown into Func1()";
    Else
        Return P;
    EndIf;
EndFunction

Async Procedure returns nothing

The Async procedure is first of all a procedure and only then it is Async. As a procedure, it does not return a value. If an uncaught exception is thrown while executing the Async procedure, this will result in an error message displayed to the user. This exception itself cannot be caught and handled by the code that called the procedure.

This feature puts Async procedures in a somewhat special position. Async procedures can be quite appropriate as command handlers and so on. Command handlers are called from the platform and almost never called from 1C:Enterprise language. And the fact that a message about an uncaught exception is displayed without any effort by the developer can be very useful.

But if there is a need to handle such exceptions by the caller, you should consider using the Asynс function.

Example

Now let's see how the new asynchrony functionality can be put into practice. Let's imagine that we need to design a form to copy files from one directory on a client computer to another. For simplicity, we assume that only files that are located directly in the source directory should be copied. To store and edit paths to directories, the form includes two attributes of the String type: SourceDirectory and TargetDirectory. Copying is called by the CopyFiles command.

To begin with, let's write a form module using synchronous platform methods, expecting that synchronous methods can be used on the client.

&AtClient
Procedure CopyFiles(Command)
    Try
        CopyFilesSync(SourceDirectory, TargetDirectory); 
    Except
        Inf = ErrorInfo();
        WarningAsync("An error occurred: " + Inf.Details); 
    EndTry
EndProcedure

&AtClient
Procedure CopyFilesSync(SourceDirectory, TargetDirectory)
    Files = FindFiles(SourceDirectory, "*", False);

    For Each File From Files Do
        SourceFile = SourceDirectory + "/" + File.Name; 
        TargetFile = TargetDirectory+ "/" + File.Name;
        CopyFile(SourceFile, TargetFile); 
    EndDo;
EndProcedure

In the given module fragment, the CopyFiles() procedure is a handler of the CopyFiles command. Actually, files are copied by the CopyFilesSync() procedure. To search for the files to be copied, the synchronous FindFiles() platform function is used. To copy each of the found files, the CopyFile() platform procedure is used. Exceptions that may be thrown from CopyFilesSync() are caught and handled in the CopyFiles() procedure.

The code is easy to understand. However, using synchronous methods on the client is often prohibited. Let's see what the code written using the new asynchrony technology might look like. 

&AtClient
Async Procedure CopyFiles(Command)
    Try
        Await CopyFilesAsync(SourceDirectory, TargetDirectory); //1
    Except
        Inf = ErrorInformation();
        WarningAsync("An error occurred: " + Inf.Details); 
    EndTry
EndProcedure

&AtClient
Async Function CopyFilesAsync(SorceDirectory, TargetDirectory)
    Files = Await FindFilesAsync(SourceDirectory, "*", False); //2

    For Each File From Files Do
        SourceFile = SourceDirectory + "/" + File.Name; 
        TargetFile = TargetDirectory + "/" + File.Name;
        Await CopyFileAsync(SourceFile, TargetFile); //3
    EndDo;
EndFunction

It is hard not to notice the similarities between "synchronous" and "asynchronous" modules. Let's focus on the differences. In asynchronous mode, files are actually copied by

Async Function CopyFilesAsync(SourceDirectory, TargetDirectory)

What was a procedure in the "synchronous" version became the Asynс function. The Async modifier appears since the new asynchronous platform functions FindFilesAsync>() and CopyFileAsync() are called inside the function, and the values returned by them are used as arguments of the Await operators.

The procedure is transformed into the function because in the CopyFiles() procedure it is necessary to catch and handle exceptions that may be thrown when executing CopyFilesAsync().

In this case, the specific value wrapped in Promise returned from the CopyFilesAsync() function upon normal completion has no value. The caller only needs to distinguish between normal and abnormal completion.

Therefore, upon normal completion of CopyFilesAsync(), Promise will wrap Undefined in all cases, as indicated by the absence of explicit value return in CopyFilesAsync(). In the CopyFiles() procedure, the result of calculating the expression

Await CopyFilesAsync(SourceDirectory, TargetDirectory); //1

is not assigned anywhere. At the same time, this operator itself is inside the Try...Except block to catch an exception that may be thrown inside CopyFilesAsync().

Whie executing

Files = Await FindFilesAsync(SourceDirectory, "*", False); //2

Promise that returned from the "new" asynchronous FindFilesAsync() platform function will go to the Await operator input. Once FindFilesAsync() in completed, the file search result will be placed in the Files variable or the Await operator will throw an exception if an error occurred while executing FindFilesAsync().

The result of calculating

Await CopyFileAsync(SourceFile, TargetFile); //3

is also not assigned anywhere. At this point in the code, you just need to wait until the file is copied and throw an exception if an error occurred while executing CopyFileAsync().

Let's try to make the task a bit more complicated and require that a message about the number of copied files is displayed upon successful completion. To do this, the CopyFilesAsynс() function must return the number of copied files to the CopyFiles() procedure, and the CopyFiles() procedure itself must display a message for the user.

An improved form module might look like this:

&AtClient
Async Procedure CopyFiles(Command)
    Try
        Counter = Await CopyFilesAsync(SourceDirectory, TargetDirectory); //1
        Report("Files copied: " + Counter); 
    Except
        Inf = ErrorInfo);
        WarningAsync("An error occurred: " + Inf.Details); 
    EndTry
EndProcedure

&AtClient
Async Function CopyFilesAsync(SourceDirectory, TargetDirectory)
    Files = Await FindFilesAsync(SourceDirectory, "*", False); //2
    Counter = 0;

    For Each File From Files Do
        SourceFile = SourceDirectory + "/" + File.Name; 
        TargetFile = TargetDirectory + "/" + File.Name;
        Await CopyFileAsync((SourceFile, TargetFile); //3
        Counter = Counter + 1;
    EndDo;

    Return Counter;
EndFunction

What's changed?

The CopyFilesAsync() function now has the local Counter variable to count the number of copied files. The value of this variable is explicitly returned from the function.

In the CopyFiles() procedure, the result of the Await operator is now assigned to the local Counter variable whose value is used when displaying a message to the user.

Finally, we can simplify the CopyFiles() procedure if this procedure does not require specific handling of exceptions and it is sufficient to just display a message to the user.

&AtClient
Async Procedure CopyFiles(Command)
    Counter = Await CopyFilesAsync(SourceDirectory, TargetDirectory); //1
    Report("Files copied: " + Counter);
EndProcedure

How it works

Above, we looked at what the "new" asynchronous code might look like. Now is the time to figure out how it works.

The main magic is hidden in the Await operator. As already mentioned, logically, the Await operator waits for the asynchronous function that follows the Promise object to complete. It might seem that this waiting means blocking the execution thread until the asynchronous function that returned Promise is completed. But it is not the case. Remember that we are dealing with the same asynchrony that is already present in 1C:Enterprise platform. But only in a different way.

The new thing is that execution of the Async procedure or function can be paused and resumed in the Await operator. Pausing means exiting the procedure or function so that it can be continued later. But the call stack element matching the paused procedure or function is not cleared or removed. When resumed, execution starts from the same point where it was paused with the same environment, including the values of local variables, as it was before pausing.

Thus, executing the Await operator looks like this:

  • The procedure or function is paused.
  • On the first pause, the code that originally called the procedure or function will work again. On subsequent pauses, the system code that resumed the execution will work again.
  • The execution is resumed as soon as Promise is completed. In this case, the Await operator returns a value or throws an exception depending on what is wrapped in Promise.

We can try to make a parallel between the "new" and "old" asynchrony. Let's look at a small snippet that copies a file using the old asynchrony technology.

&AtClient
Procedure CopyFile(SourceFile, TargetFile)
    Details = New NotifyDescription("AfterCopy, ThisObject);
    BeginCopyingFile(Details, SourceFile, TargetFile);
EndProcedure

Procedure AfterCopy(File, AddlParam) Export
    Report("File copied: " + File);
EndProcedure

Looking at this example, we can say that execution is paused while executing the "old" asynchronous BeginCopyingFile() procedure, then resumed when this procedure is completed, and completed when the AfterCopy() procedure is called.

And now the same snippet written in a new way.

&AtClient
Async Function CopyFile(SourceFile, TargetFile)
    Pr = CopyFileAsync(SourceFile, TargetFile);
    Await Pr;
    Report("File copied: " + TargetFile);
EndFunction

In this example, execution is paused in the Await operator. The execution is resumed once CopyFileAsync() is completed in the same operator. That is, the Await operator allows you to set a pause or resume point almost anywhere in the code without creating a separate procedure, the NotifyDescription() object, and so on.

Let's look at the example to see what happens while executing the "new" asynchronous code. As an example, let's take an already known form module that copies files from one directory to another. For better clarity, we will rewrite it just a little bit.

&AtClient
Async Procedure CopyFiles(Command)
    Pr = CopyFilesAsync(SourceDirectory, TargetDirectory); //(1)
    Counter = Await Pr; //(4) (10)
    Report("Files copied: " + Counter);
    Return; //(11)
EndProcedure

&AtClient
Async Function CopyFilesAsync(SourceDirectory, TargetDirectory)
    Pr1 = FindFilesAsync(SourceDirectory, "*", False); //(2)
    Files = Await Pr1; //(3) (5)
    Counter = 0;

    For Each File From Files Do
        SourceFile = SourceDirectory + "/" + File.Name; 
        TargetFile = TargetDirectory + "/" + File.Name;
        Pr2 = CopyFileAsync(SourceFile, TargetFile); //(6)
        Await Pr2; //(7) (8)
        Counter = Counter + 1; 
    EndDo;

    Return Counter; //(9)
EndFunction

Lines of interest are commented. Each of the numbers in parentheses represents a sequence number in the sequence of code execution.

  • Start executing the CopyFiles() procedure. Call the CopyFilesAsync() function.
  • Start executing the CopyFilesAsync() function. Call the asynchronous FindFilesAsync() platform function.
  • Stop the Await operator until Pr1 is completed. Return to the CopyFiles() procedure.
  • Stop the Await operator until Pr (CopyFilesAsync()) is completed.
  • Resume executing CopyFilesAsync() due to completion of Pr1 (FindFilesAsync()). The Files variable is assigned the search result. If an error occurs while executing FindFilesAsync(), an exception is thrown.
  • Call the asynchronous CopyFileAsync() platform function.
  • Pause the CopyFilesAsync() function until Pr2 (CopyFileAsync()) completes.
  • Resume executing CopyFilesAsync() due to completion of Pr2 (CopyFileAsync()). If an error occurs while executing CopyFileAsync(), an exception is thrown.
  • Finally exit CopyFilesAsynс(). Promise returned by the CopyFilesAsync() function in step (3) enters the Success state and becomes a container for the value contained in the Counter variable.
  • Resume executing the CopyFiles() procedure due to completion of Pr (CopyFilesAsync()). The Counter variable is assigned the number of copied files. If an uncaught exception was thrown while executing CopyFilesAsync(), the same exception is thrown.
  • Finally exit the CopyFiles() procedure.

It is clear that steps (6), (7), and (8) can be performed several times depending on the number of files being copied.

Conclusion

This article has attempted to provide a short but comprehensive overview of the new mechanism to deal with asynchrony. It is short only in the sense that it does not take up too much space. At the same time, we want to think that sufficient consideration has been given to all the necessary details.

 

Icon/Social/001 Icon/Social/006 Icon/Social/005 Icon/Social/004 Icon/Social/002