How to Simplify Asynchronous JavaScript using the Result

您所在的位置:网站首页 resulterror How to Simplify Asynchronous JavaScript using the Result

How to Simplify Asynchronous JavaScript using the Result

#How to Simplify Asynchronous JavaScript using the Result| 来源: 网络整理| 查看: 265

Over the last 18 years of programming, I've had to deal with asynchronous behavior in virtually every project.

Since the adoption of async-await in JavaScript, we've learned that async-await makes a lot of code more pleasant and easier to reason about.

Recently I noticed that when I work with a resource that needs to asynchronously connect and disconnect, I end up writing code like this:

// NOT MY FAVORITE PATTERN router.get('/users/:id', async (req, res) => { const client = new Client(); let user; try { await client.connect(); user = await client.find('users').where('id', req.path.id); } catch(error) { res.status(500); user = { error }; } finally { await client.close(); } res.json(user); });

It gets verbose because we have to use try/catch to handle errors.

Examples of such resources include databases, ElasticSearch, command lines, and ssh.

In those use cases, I've settled into a code pattern I'm calling the Result-Error Pattern.

Consider rewriting the code above like this:

// I LIKE THIS PATTERN BETTER router.get('/users/:id', async (req, res) => { const { result: user, error } = await withDbClient(client => { return client.find('users').where('id', req.path.id); }); if (error) { res.status(500); } res.json({ user, error }); });

Notice a few things:

The database client gets created for us and our callback can just utilize it.Instead of capturing errors in a try-catch block, we rely on withDbClient to return errors.The result is always called result because our callback may return any kind of data.We don't have to close the resource.

So what does withDbClient do?

It handles creating the resource, connecting and closing.It handles try, catch, and finally.It ensures that there will be no uncaught exceptions thrown from withDbClient.It ensures that any exceptions thrown in the handler also get caught inside withDbClient.It ensures that { result, error } will always be returned.

Here is an example implementation:

// EXAMPLE IMPLEMENTATION async function withDbClient(handler) { const client = new DbClient(); let result = null; let error = null; try { await client.connect(); result = await handler(client); } catch (e) { error = e; } finally { await client.close(); } return { result, error }; } A step furtherpexels-tom-fisk-1595104Photo by Tom Fisk from Pexels

What about a resource that does not need to be closed? Well the Result-Error Pattern can still be nice!

Consider the following use of fetch:

// THIS IS NICE AND SHORT const { data, error, response } = await fetchJson('/users/123');

Its implementation might be the following:

// EXAMPLE IMPLEMENTATION async function fetchJson(...args) { let data = null; let error = null; let response = null; try { const response = await fetch(...args); if (response.ok) { try { data = await response.json(); } catch (e) { // not json } } else { // note that statusText is always "" in HTTP2 error = `${response.status} ${response.statusText}`; } } catch(e) { error = e; } return { data, error, response }; } Higher-level useaerial-g3ccde9887_1920Photo by 16018388 from Pixabay

We don't have to stop at low-level use. What about other functions that may end with a result or error?

Recently, I wrote an app with a lot of ElasticSearch interactions. I decided to also use the Result-Error pattern on higher-level functions.

For instance, searching for posts produces an array of ElasticSearch documents and returns result and error like this:

const { result, error, details } = await findPosts(query);

If you've worked with ElasticSearch, you'll know that responses are verbose and data is nested several layers inside the response. Here, result is an object containing:

records – An Array of documentstotal – The total number of documents if a limit was not appliedaggregations – ElasticSearch faceted-search information

As you might guess, error may be an error message and details is the full ElasticSearch response in case you need things like error metadata, highlights, or query time.

My implementation for searching ElasticSearch with a query object reads something like this:

// Fetch from the given index name with the given query async function query(index, query) { // Our Result-Error Pattern at the low level const { result, error } = await withEsClient(client => { return client.search({ index, body: query.getQuery(), }); }); // Returning a similar object also with result-error return { result: formatRecords(result), error, details: result || error?.meta, }; } // Extract records from responses function formatRecords(result) { // Notice how deep ElasticSearch buries results? if (result?.body?.hits?.hits) { const records = []; for (const hit of result.body.hits.hits) { records.push(hit._source); } return { records, total: result.body.hits.total?.value || 0, aggregations: result.aggregations, }; } else { return { records: [], total: null, aggregations: null }; } }

And then the findPosts function becomes something simple like this:

function findPosts(query) { return query('posts', query); }Summary

Here are the key aspects of a function that implements the Result-Error Pattern:

Never throw exceptions.Always return an object with the results and the error, where one may be null.Hide away any asynchronous resource creation or cleanup.

And here are the corresponding benefits of calling functions that implement the Result-Error Pattern:

You don't need to use try-catch blocks.Handling error cases is as simple as if (error).You don't need to worry about setup or cleanup operations.

Don't take my word for it, try it yourself!



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3