Twenty One Media
webJune 20, 2026

The User Email Is a Promise. The Airtable Record Isn't.

Every form submission on our site hits an API route that does three things: sends a confirmation email to the person who submitted, fires a notification email to us, and writes a lead record to Airtable. Three operations, one request.

The naive version awaits them one at a time and returns an error if any of them fail. That means an Airtable outage breaks your contact form from the user's perspective. That's wrong.

The Pattern

const [userResult, notifyResult, leadResult] = await Promise.all([
  sendUser,
  sendNotify,
  recordLead,
]);

if (!leadResult.ok) {
  console.error("[audit-request] Airtable capture failed:", leadResult.reason);
}

if (userResult.error) {
  console.error("[audit-request] User email failed:", userResult.error);
  return NextResponse.json(
    { error: "Failed to send confirmation" },
    { status: 502 }
  );
}

if (notifyResult.error) {
  console.error("[toolkit-signup] Notification email failed (non-fatal):", notifyResult.error);
}

return NextResponse.json({ success: true });

Promise.all starts all three at once. They run in parallel. But the failure checks after it treat them differently.

The user email check comes first and is the only one that returns a 502. If we can't send the person a confirmation, the route fails. Everything else logs and continues.

Why the Split

The user email is a contract. The person clicked submit expecting a confirmation to land in their inbox. If it doesn't, we broke that expectation. From their perspective, the form failed. Returning 200 when their confirmation isn't going out would be lying.

The notify email is for us. If it fails, we miss a ping. The form still worked for the user. Log the failure, fix the underlying issue, move on.

The Airtable record is also for us. It's attribution data and a pre-call prep reference. Important, but it's our problem if it's missing, not theirs. Our captureLead function is already designed to never throw — it catches internally and returns { ok: false, reason: "..." } — so the route just logs and continues.

The Ordering Matters

After Promise.all resolves, we check Airtable first. That's not because it's more important — it's because we want to log it regardless of what happens next. If we checked user email first and returned the 502, the Airtable failure would never get logged.

The guard order is: log the non-fatal failures, then check the fatal one, then return success.

Applied Across Five Routes

Every form route on the site follows this structure. The audit request, the toolkit signup, the Operations Engine inquiry, the skill clone, the contact form. Each one has its own set of side effects, and each categorizes them the same way: is this the user's deliverable, or is this ours?

The contact form is simpler — it sends one email to us with no confirmation to the user — so it has a single send call and returns 502 if that fails. No parallelism needed, no split.

The toolkit signup sends the repo link to the user. That's their deliverable. Same pattern: if it fails, 502. If the notify fails, log and continue.

The Rule of Thumb

Before writing error handling in an API route, ask: if this specific thing fails, does the user care? If yes, return an error status. If no, log it and let the response succeed.

Promise.all gives you the parallelism. Differentiated error handling gives you the honesty.