How should our app handle a situation where it needs a service that can take some time to return its result, for example, when we have to fetch or store data, read a large file, or need a response from a web service. Obviously, the app can't wait for this to end and the result to arrive (the so-called synchronous way), because this would freeze the screen (and the app) and frustrate users. A responsive web app must be able to call this service and immediately continue with what it was doing. Only when the result returns should it react and call some function on the response data. This is called working in an asynchronous, non-blocking way. Most of the methods in dart:io
for server-side Dart apps work in this way.
Developers with a Java or C# background would perhaps think of starting another thread to do additional work, but Dart can't do it. Dart has to compile to JavaScript, so similar to JavaScript, it also works in a single-threaded model that is tightly-controlled by the browser's event loop. On the Web (client as well as server), the code has to execute as asynchronously as possible in order to not block the browser from serving its user or a server process from serving its many thousands of client requests. The JavaScript world has long solved this issue using callbacks; this is a function that is "called" when the result of the first function called returns ("backs"). In the following code snippet, the first function that will return the result is doStuff
and handle
is registered as a callback that will work on the result; when an error occurs (onError
), handleError
is invoked:
doStuff((result) { handle(result); }, onError: (e) { handleError(e); });
The same mechanism can be used in Dart; but, here, we have a more elegant way to handle this with objects appropriately called the Future
objects. Now, we will define doStuff
to return a Future
object; this is a value (which could be an error) that is not yet available when doStuff
returns. However, it will be available sometime in the future after doStuff
has been executed (indicated using the then
keyword). The same code snippet written using the Future
objects will then be much more readable:
doStuff() .then( (result) => handle(result) ) .catchError( (e) => handleError(e) );
The doStuff
method returns a Future
object, so it could have been written as:
Future fut1 = doStuff(); fut1.then( (result) => handle(result) ) .catchError( (e) => handleError(e) );
However, the first or even the following shorter way is idiomatically used:
doStuff() .then(handle) .catchError(handleError);
Then, it registers the handle
callback and catchError
calls handleError
when an error occurs, which stops the error from propagating. It could be considered as the asynchronous version of a try/catch construct (there is also a .whenComplete
handler that is always executed and it corresponds with finally). The syntax advantage becomes even clearer when callbacks are nested to enforce the execution order, because this results in ugly and difficult-to-read code (sometimes referred to as callback hell). Suppose a doStuff2
computation has to occur between doStuff
and handle
, the first snippet will become much less readable:
doStuff((result){ doStuff2((result){ handle((result) { }); }, onError: (e) { handleError(e); }); }, onError: (e) { handleError(e); });
However, the version using the Future
objects remains very simple:
doStuff() .then(doStuff2) .then(handle) .catchError(handleError);
Through this chaining syntax, it looks like synchronous code, but it is purely an asynchronous code executing; catchError
catches any errors that occur in the chain of the Future
objects. As a simple, but working, example, suppose a future1
app needs to show or process a large bigfile.txt
file and we don't want to wait until this I/O is completely done:
import 'dart:io'; (1) import 'dart:async'; var file; (2) main() { file = new File('bigfile.txt'), (3) // using Future: readFileFuture(); // using async / await readFileAsync(); .then( (text) => print(text) ) .catchError( (e) => print(e) ); // do other things while file is read in ... (7) } readFileFuture() { file.readAsString() (4) .then((text) => print(text)) (5) .catchError((e) => print(e)); (6) // shorter version: // file.readAsString() // .then(print) // .catchError(print); } readFileAsync() async { try { var text = await file.readAsString(); await print(text); } catch (e) { print(e); } }
To work with files and directories, dart:io
is needed in line (1)
; the Future
functionality comes from dart:async
(line (2)
). In line (3)
, a File
object is created. In line (4)
, the action to read the file is started asynchronously. However, the program immediately continues executing lines (7)
and beyond. When the file is completely read through, line (5)
prints its contents; should an e
error (for example, a nonexisting file) have occurred, it is printed in line (6)
. You can even leave out the intermediary variables and write:
file.readAsString() .then(print) .catchError(print);
In the readFileAsync
method, we do the same, but now with the newer async / await
syntax (see Chapter 3, Structuring Code with Classes and Libraries). Error handling can be applied here with normal try/catch
. Asynchronous functions return the Future
objects, so it will boil down to the same mechanism, but the syntax will be more readable.
You can find more information at https://www.dartlang.org/articles/futures-and-error-handling/.
3.139.67.5