Node.js use event-driven, non-blocking I/O model, a lot of interfaces are asynchronous, like file operations, network request。 though syncchronous methods are provided, we should avoid to use them as much as possible becuase they are blocking operations.
The official asynchronous interface use the callback form:
const fs = require('fs');
fs.readFile(filepath, 'utf8', (err, content) => {
if(err) return ;
...
})
While the concept of callbacks is simple, it can easily lead to problem call callback hell when business logic get complicated. to solve this, methods like event、thunk、Promise、Generator function、Async functions comes up successively, and finally Async Functions
wins out which also supports by ThinkJS.
Async functions use async/await
syntax, for example:
async function fn() {
const value = await getFromApi();
doSomethimgWithValue();
}
Async/await is base on Promise, if expression follows await is not Promise, we need a way to turn it into a Promise.
ThinkJS 3.0 suggest use Async functions, and all framework interface are base on Promise which is handy to apply Async functions.
module.exports = class extends think.Controller {
async indexAction() {
// select return Promise,use await
const list = await this.model('user').select();
return this.success(list);
}
}
Though Async functions is handy for Asynchronous issues, but it requires Node.js version >=7.6.0
, to use it in lower version of Node.js, we can use Babel to tranform (Framework support Node.js version greater than 6.0, so cli default ship with Babel transform which truns Async functions into a way called Generator functions + co).
Async functions syntax look very similar to Generator syntax, there are quit some differences between them. Async functions:
async/await
has better text semantics。while Generator which is a iterator can be use to handle async problem.Async functions is base on Promise, but a lot api is not returning Promise, like Node.js is using callback which signature is fn(aa, bb, callback(err, data))
, we can convert it into Promise way using framework method think.promisify
.
const fs = require('fs');
const readFile = think.promisify(fs.readFile, fs);
const parseFile = async (filepath) => {
const content = await readFile(filepath, 'utf8'); // readFile returns Promise
doSomethingWithContent();
}
If the callback is not callback(err, data)
, instead of think.promisify
, we need to handle it seperatly:
const exec = require('child_process').exec;
return new Promise((resolve, reject) => {
// exec callback params
exec(filepath, (err, stdout, stderr) => {
if(err) return reject(err);
if(stderr) return reject(stderr);
resolve(stdout);
})
})
Error handling is very troublesome in Node.js, a little mistake may cause respose failure. we need to take care of each callback's erro separately which is very trivial.
By using Async functions, error will be converted to Rejected Promise, which will block the following excution.
Use try/catch
for Async function is no different then synchronous try/catch:
module.exports = class extends think.Contoller {
async indexAction() {
try {
await getDataFromApi1();
await getDataFromApi2();
await getDataFromApi3();
} catch(e) {
// capture error
}
}
}
Wrapper everything inside try/catch
to catch error. One problem is it is not easy to tell which interface trigger the error in catch, to judge by error type and wrap eash call is too ugly. For this we can use then/catch
to handle.
For promise, we know there are then and catch method, each is use to handle resovle and reject. So we can precent the error by using catch and ture Rejected Promise to Resolved Promise, and then handle the error properly.
module.exports = class extends think.Controller {
async indexAction() {
// Use catch to convert rejected promise to resolved promise
const result = await getDataFromApi1().catch(err => {
return think.isError(err) ? err : new Error(err)
});
// handle error type
if (think.isError(result)) {
// return error message or return formatted error message
return this.fail(1000, result.message);
}
const result2 = await getDataFromApi2().catch(err => {
return think.isError(err) ? err : new Error(err)
});
if(think.isError(result2)) {
return this.fail(1001, result.message);
}
// return false in catch if error message is not used
// but the condition is getDataFromApi3 will not return false
const result3 = await getDataFromApi3().catch(() => false);
if(result3 === false) {
return this.fail(1002, 'error message');
}
}
}
Use catch to trun Rejected Promise to Resolved Promise, you can easily customize the way to output error message.
In some situation, it is not convenient to add try/catch neither to turn Rejected Promise to Resolved Promise in catch, then you can use framework middleware trace to handle error message.
// src/config/middleware.js
module.exports = [
...
{
handle: 'trace',
options: {
sourceMap: false,
debug: true, // print error messgae or not
error(err) {
// particular error handling, like update to monitor system.
console.error(err);
}
}
}
...
];
Whenever throws an error, trace module will catch it. In debug mode, it will display detail error information and response data as request content type.
To delay processing some transactions, the most common way is use setTimeout
method, but setTimeout itself doesn't return Promise, if error throw inside the function will not be catchable, we can convert it to Promise.
Frame provide think.timeout
to quickly use a promisify timeout:
return think.timeout(3000).then(() => {
// 3s later goes here
})
or:
module.exports = class extends think.Controller {
async indexAction() {
await think.timeout(3000);// wait 3000 miliseconds
return this.success();
}
}
No,ThinkJS 3.x no longer support Generator, all asynchronous handling use Async funtions with Promise, which is also the most elegent solution so far.