Async/Await & Generator Middleware Composition
The `composition` package provides a utility for composing asynchronous middleware functions in a manner similar to Koa. It uniquely supports mixing both traditional generator functions (using `yield next`) and modern `async/await` functions within the same middleware stack. Each composed middleware function is expected to return a Promise or handle `next()` correctly to ensure the control flow propagates. The package is currently at version 2.3.0, indicating a stable release focused on robust async control flow. While no explicit release cadence is documented, utilities of this nature tend to be stable with updates driven by significant changes in JavaScript's asynchronous patterns or bug fixes. Its key differentiator is the seamless interoperability between generator-based and promise-based async middleware, making it suitable for projects transitioning or requiring compatibility across different asynchronous programming styles.
Common errors
-
TypeError: next is not a function
cause A middleware function in the stack failed to correctly call `next()`, or the `next` argument was not properly propagated through the middleware chain.fixReview all middleware functions to ensure `next` is correctly invoked and awaited/yielded. Verify that the function signature includes `next` as a parameter if it's being called. -
UnhandledPromiseRejectionWarning: Unhandled promise rejection.
cause A Promise within the middleware chain rejected, and the rejection was not explicitly caught by a `.catch()` block in the composed function's invocation or any preceding middleware.fixAdd a `.catch()` handler to the invocation of the composed function (e.g., `fn().catch(err => console.error(err))`) to gracefully handle any errors that propagate out of the middleware stack. -
SyntaxError: await is only valid in async functions and the top level bodies of modules
cause Attempting to use the `await` keyword outside of an `async` function in an environment that does not support top-level await, or within a non-async middleware function.fixEnsure `await` is only used inside functions declared with the `async` keyword. If using `async/await` in older Node.js versions, ensure you are using Node.js v7.6.0 or newer.
Warnings
- gotcha Middleware functions must explicitly call `next()` and ensure their return value, or the result of `next()`, is a Promise that is either returned or awaited. Failure to do so will break the middleware chain, preventing subsequent middleware from executing or causing unexpected behavior.
- gotcha Mixing generator functions and `async/await` functions within the same stack requires a Node.js environment that supports both. Older Node.js versions (e.g., prior to v7.6.0) may not support `async/await` natively, and generators themselves require specific syntax support.
- gotcha The `this` context for middleware functions is explicitly set when the composed function is invoked (e.g., `fn.call(thisArg)`). If middleware relies on a specific `this` context and it's not provided or is incorrect, it can lead to `TypeError` or unexpected runtime errors.
Install
-
npm install composition -
yarn add composition -
pnpm add composition
Imports
- compose
import { compose } from 'composition';import compose from 'composition';
- compose
const { compose } = require('composition');const compose = require('composition'); - MiddlewareFunction
import type { MiddlewareFunction } from 'composition';
Quickstart
var compose = require('composition');
var stack = [];
// Middleware 1: A generator function
stack.push(function* (next) {
console.log('Middleware 1 (generator): Before calling next()');
yield next;
console.log('Middleware 1 (generator): After next() returned');
});
// Middleware 2: A regular function returning a Promise
stack.push(function (next) {
console.log('Middleware 2 (promise): Before calling next()');
return Promise.resolve('Data from Middleware 2').then((data) => {
console.log('Middleware 2 (promise): Received data:', data);
return next(); // Ensure next is called and its result is returned as a Promise
}).then(() => {
console.log('Middleware 2 (promise): After next() returned');
return 'Modified by Middleware 2';
});
});
// Middleware 3: An async/await function
stack.push(async function (next) {
console.log('Middleware 3 (async/await): Before calling next()');
const result = await next(); // Await the result of the downstream middleware
console.log('Middleware 3 (async/await): After next() returned with:', result);
return 'Final value from Middleware 3';
});
// Compose the middleware stack into a single function
var fn = compose(stack);
// Invoke the composed function, which returns a Promise
fn.call({ customContext: 'hello' }).then(function (finalVal) {
console.log('Composed function resolved with final value:', finalVal);
}).catch(function (err) {
console.error('Composed function caught an error:', err.stack);
process.exit(1);
});