SEMA Pattern: Run Multiple Async Effects from One Trigger
SEMA launches multiple independent async actions from a single trigger and collects every outcome with Promise.allSettled().
09 Jun 2026, 15:16 UTC

One Click, Three Async Jobs
Imagine a registration endpoint. When a user submits the form, the server must create a user record, add the contact to a marketing CRM, and queue a welcome email. If you await each call one after another, the user waits for the slowest network call three times over. If you fire them all and forget, a CRM outage leaves the user registered but invisible to your marketing list.
The useful takeaway is to start every effect from a single trigger, wait for all of them to settle, then inspect each result individually. This is the SEMA pattern—Single Effect, Multiple Action—in practice.
SEMA with Promise.allSettled
Unlike Promise.all(), which rejects as soon as one effect fails, Promise.allSettled() waits until every promise completes, whether fulfilled or rejected. That lets you report partial success instead of discarding everything because one email provider timed out.
type SemaEffect = () => Promise;
async function runSema(effects: SemaEffect[]): Promise<{ ok: T[]; failed: Error[] }> {
const settled = await Promise.allSettled(effects.map((fn) => fn()));
const ok: T[] = [];
const failed: Error[] = [];
for (const result of settled) {
if (result.status === "fulfilled") {
ok.push(result.value);
} else {
failed.push(result.reason);
}
}
return { ok, failed };
}
Here is a registration handler that uses it:
import { runSema } from "./sema";
async function createUser(email: string) { /* ... */ }
async function addToCrm(email: string) { /* ... */ }
async function sendWelcomeEmail(email: string) { /* ... */ }
export async function handleRegister(email: string) {
const { ok, failed } = await runSema([
() => createUser(email),
() => addToCrm(email),
() => sendWelcomeEmail(email),
]);
if (failed.length > 0) {
console.error("Registration partial failure:", failed.map((e) => e.message));
}
return {
userCreated: ok.length >= 1,
crmUpdated: ok.length >= 2,
emailQueued: ok.length === 3,
errors: failed.length,
};
}
Trade-off: Memory and Failure Visibility
SEMA runs effects concurrently, so memory usage grows with the number of simultaneous promises. If you batch 10,000 file uploads this way, you may exhaust available file descriptors or memory before any of them finish. For large batches, process in chunks or use a concurrency limiter instead.
Another limitation is that Promise.allSettled() tells you that something failed, but not necessarily why in business terms. Wrap each effect so it throws a typed error with context, such as CrmUnavailableError, so the caller can decide whether to retry only the CRM step.
Verify Before You Ship
Run this script in your project directory with Node.js 18 or later. No special permissions are required.
node --version
const slow = () => new Promise((r) => setTimeout(() => r("user"), 50));
const flaky = () => new Promise((_, rej) => setTimeout(() => rej(new Error("crm down")), 30));
const fast = () => new Promise((r) => setTimeout(() => r("email"), 10));
async function main() {
const start = Date.now();
const result = await runSema([slow, flaky, fast]);
console.log("elapsed ms:", Date.now() - start);
console.log("ok:", result.ok);
console.log("failed:", result.failed.map((e) => e.message));
}
main();
Expected check: elapsed time should be close to the slowest effect (about 50 ms), not the sum of all three. The failed array should contain the CRM error while the other two results remain available.
If a single effect must block the response—such as creating the user record before sending email—do not use SEMA for that pair. Sequence the dependent calls and use SEMA only for independent side effects that can safely complete in any order.
Closing
SEMA is not about making every operation atomic. It is about launching multiple independent async actions from one trigger, collecting every outcome, and deciding what partial success means for your users. Start with Promise.allSettled(), log individual failures, and verify timing with a small script before you roll it out.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.