resend
semver
>=3.0.0postconditions15functions14last verified2026-06-11coverage score100%Postconditions — what we check
- send · send-no-error-checkerrorWhenemails.send() called without checking result.error — API failure, rate limit, invalid address, or network error returns { data: null, error: ErrorResponse } silentlyThrows
Does not throw. Returns { data: null, error: { name, message, statusCode } } on failure. Unchecked: email silently not sent with no visible error in application.Required handlingCaller MUST check result.error after every emails.send() call. Two acceptable patterns: Pattern 1 — destructure and check: const { data, error } = await resend.emails.send({...}); if (error) { handle error; } Pattern 2 — assign and check: const result = await resend.emails.send({...}); if (result.error) { handle error; } Pattern 3 — try-catch (also acceptable, defensive): try { const result = await resend.emails.send({...}); if (result.error) { handle error; } } catch (err) { handle unexpected error; } DO NOT return result to caller without checking result.error first. DO NOT access result.data without checking result.error first.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[1] - batch.send · batch-send-no-error-checkerrorWhenbatch.send() called without checking result.error — entire batch silently fails if API call returns an errorThrows
Does not throw. Returns { data: null, error: ErrorResponse } on failure. All emails in batch are unsent if error is unchecked.Required handlingCaller MUST check result.error after every batch.send() call. Batch failures affect all emails — no partial success. Log error with batch size and recipient count for debugging.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[2] - webhooks.verify · webhooks-verify-no-try-catcherrorWhenwebhooks.verify() called without try-catch — WebhookVerificationError thrown on invalid signature, expired timestamp, or missing headers is not caughtThrows
Throws WebhookVerificationError (from standardwebhooks package, extends Error) on: - "Missing required headers" — svix-id, svix-timestamp, or svix-signature missing - "Invalid Signature Headers" — svix-timestamp is not a valid integer - "Message timestamp too old" — webhook timestamp > 300 seconds in the past - "Message timestamp too new" — webhook timestamp > 300 seconds in the future - "No matching signature found" — HMAC signature does not match payload Also throws plain Error("Secret can't be empty.") if webhookSecret is empty string. UNLIKE all other resend methods, webhooks.verify() does NOT return { data, error }. It either returns the parsed WebhookEventPayload or throws.Required handlingCaller MUST wrap webhooks.verify() in try-catch: try { const payload = resend.webhooks.verify({ payload: rawBody, headers: { id, timestamp, signature }, webhookSecret: process.env.RESEND_WEBHOOK_SECRET!, }); // process payload } catch (err) { if (err instanceof WebhookVerificationError) { return res.status(401).json({ error: 'Invalid webhook signature' }); } throw err; } DO NOT call webhooks.verify() without try-catch. DO NOT trust the payload or process the event if verify() throws. Return HTTP 401 on WebhookVerificationError — do not process the webhook.costhighin prodimmediate exceptionusers seeservice unavailablevisibilitysilentSources[3] - webhooks.verify · webhooks-verify-missing-secreterrorWhenwebhooks.verify() called with empty or undefined webhookSecret — throws Error("Secret can't be empty.") before any signature check occursThrows
Throws plain Error("Secret can't be empty.") when webhookSecret is an empty string. Throws TypeError when webhookSecret is undefined (cannot read property of undefined).Required handlingMUST validate that process.env.RESEND_WEBHOOK_SECRET is set before calling verify(). Crash at startup (not at request time) if the secret is missing: if (!process.env.RESEND_WEBHOOK_SECRET) { throw new Error('RESEND_WEBHOOK_SECRET environment variable is required'); }costcriticalin prodimmediate exceptionusers seesecurity breachvisibilitysilentSources[3] - emails.cancel · emails-cancel-no-error-checkwarningWhenemails.cancel() called without checking result.error — silent failure if email not found, already sent, or API key lacks permissionThrows
Does not throw. Returns { data: null, error: { name, message, statusCode } } on failure. Common errors: name='not_found' (404) — email_id does not exist or was already sent. name='restricted_api_key' (401) — API key only has sending_access, not full_access.Required handlingCaller MUST check result.error after emails.cancel(): const { data, error } = await resend.emails.cancel(emailId); if (error) { if (error.name === 'not_found') { // Email already sent or never existed — log and continue } else { throw new Error(`Failed to cancel email: ${error.message}`); } } DO NOT assume cancel succeeded without checking result.error. Handle not_found gracefully (email may have already been sent).costmediumin prodsilent failureusers seelost transactionvisibilitysilentSources[3] - contacts.create · contacts-create-no-error-checkwarningWhencontacts.create() called without checking result.error — contact not added to segment silently if validation fails, API key is restricted, or segment not foundThrows
Does not throw. Returns { data: null, error: { name, message, statusCode } } on failure. Common errors: - name='validation_error' (400) — email format invalid, required fields missing - name='missing_required_field' (422) — email field not provided - name='restricted_api_key' (401) — API key only allows sending_access, not contact management - name='not_found' (404) — segment/audience ID does not existRequired handlingCaller MUST check result.error after contacts.create(): const { data, error } = await resend.contacts.create({ email: user.email, firstName: user.name, segments: [{ id: process.env.RESEND_SEGMENT_ID }], }); if (error) { logger.error('Failed to add contact to Resend', { error, email: user.email }); // Do NOT throw — contact sync failure should not block user registration } Contact sync failures should be logged but should NOT block user-facing operations. Missing result.error check causes silent contact sync failures that corrupt marketing lists.costmediumin prodsilent failureusers seelost datavisibilitysilentSources[3] - broadcasts.send · broadcasts-send-no-error-checkerrorWhenbroadcasts.send() called without checking result.error — entire mass email silently fails if domain unverified, quota exceeded, or broadcast already sentThrows
Does not throw. Returns { data: null, error: { name, message, statusCode } } on failure. Critical errors: - name='invalid_from_address' (422) — sender domain not verified in Resend - name='monthly_quota_exceeded' (429) — monthly sending quota hit (common for large lists) - name='rate_limit_exceeded' (429) — too many API calls (5 req/sec limit per team) - name='not_found' (404) — broadcast ID does not exist - name='validation_error' (400/403) — broadcast in wrong status (already sent/queued)Required handlingCaller MUST check result.error after broadcasts.send(): const { data, error } = await resend.broadcasts.send(broadcastId); if (error) { logger.error('Broadcast send failed', { broadcastId, errorName: error.name, errorMessage: error.message, statusCode: error.statusCode, }); if (error.name === 'monthly_quota_exceeded') { await notifyTeam('Monthly email quota exceeded — broadcast not sent'); } throw new Error(`Broadcast send failed: ${error.message}`); } ALWAYS log broadcast send results with broadcastId for audit trail. monthly_quota_exceeded requires immediate team notification — quota resets monthly.costhighin prodsilent failureusers seelost transactionvisibilitysilentSources[3] - emails.create · emails-create-no-error-checkerrorWhenemails.create() called without checking result.error — API failure, rate limit, unverified domain, or network error returns { data: null, error: ErrorResponse } silently. Behaviorally identical to emails.send() — the underlying HTTP POST /emails endpoint is the same.Throws
Does not throw. Returns { data: null, error: { name, message, statusCode } } on failure. Same error vocabulary as emails.send(): invalid_from_address (422), validation_error (400/403), monthly_quota_exceeded (429), rate_limit_exceeded (429), missing_required_field (422), restricted_api_key (401), application_error (500).Required handlingCaller MUST check result.error after every emails.create() call. Same patterns as emails.send(): const { data, error } = await resend.emails.create({...}); if (error) { handle error; } OR wrap in try-catch as defensive double-check. DO NOT return result.data without checking result.error first.costmediumin prodsilent failureusers seeservice unavailablevisibilitysilent - emails.update · emails-update-no-error-checkwarningWhenemails.update() called without checking result.error — silent failure if email_id not found (404 not_found), email already sent (validation_error 400), API key restricted (401 restricted_api_key), or scheduled_at format invalid (422 validation_error).Throws
Does not throw. Returns { data: null, error: { name, message, statusCode } } on failure. Common errors: - name='not_found' (404) — email_id does not exist or was already sent - name='validation_error' (400) — email is not in scheduled state (already sent/queued/canceled) - name='restricted_api_key' (401) — API key only has sending_access, not full_access - name='validation_error' (422) — scheduled_at not a valid ISO timestampRequired handlingCaller MUST check result.error after every emails.update() call: const { data, error } = await resend.emails.update({ id: emailId, scheduledAt: newTime.toISOString(), }); if (error) { if (error.name === 'not_found') { // Email already sent or never scheduled — log + treat as no-op } else if (error.name === 'validation_error') { throw new Error(`Cannot reschedule: ${error.message}`); } else { throw new Error(`Failed to update scheduled email: ${error.message}`); } } DO NOT assume update succeeded without checking result.error. Silent failures here cause drip campaigns to fire at the wrong time and reminder emails to land before the rescheduled window.costmediumin prodsilent failureusers seedegraded performancevisibilitysilent - batch.create · batch-create-no-error-checkerrorWhenbatch.create() called without checking result.error — entire batch silently fails if request returns an error. Same risk profile as batch.send().Throws
Does not throw. Returns { data: null, error: ErrorResponse } on failure. All emails in batch are unsent if error is unchecked.Required handlingCaller MUST check result.error after every batch.create() call: const { data, error } = await resend.batch.create([...]); if (error) { logger.error('Batch send failed', { batchSize: emails.length, error }); throw new Error(`Batch failed: ${error.message}`); } Batch failures affect all emails — no partial success at the request layer. Log error with batch size for debugging.costmediumin prodsilent failureusers seeservice unavailablevisibilitysilent - domains.verify · domains-verify-no-error-checkerrorWhendomains.verify() called without checking result.error — silent failure if domain_id not found (404), API key restricted (401), or backend verification trigger errored (500).Throws
Does not throw. Returns { data: null, error: { name, message, statusCode } } on failure. Common errors: - name='not_found' (404) — domain_id does not exist in the team - name='restricted_api_key' (401) — API key only has sending_access, not domain management - name='validation_error' (400) — domain in wrong status (already verified, or not_started) - name='internal_server_error' (500) — verification trigger failed at backendRequired handlingCaller MUST check result.error after every domains.verify() call: const { data, error } = await resend.domains.verify(domainId); if (error) { logger.error('Domain verification trigger failed', { domainId, error }); throw new Error(`Cannot verify domain: ${error.message}`); } // NOTE: success here means "verification triggered" — poll domains.get(id) // for status until status === 'verified' before allowing sends from this domain. Silent failure means the user thinks verification is in progress but it never started — downstream emails.send() calls fail with invalid_from_address and the user has no signal that the verify call itself silently failed.costhighin prodsilent failureusers seeservice unavailablevisibilitysilent - broadcasts.create · broadcasts-create-no-error-checkerrorWhenbroadcasts.create() called without checking result.error — silent failure if domain unverified (422 invalid_from_address), segment_id not found (404 not_found), API key restricted (401), or required fields missing (422 missing_required_field).Throws
Does not throw. Returns { data: null, error: { name, message, statusCode } } on failure. Common errors: - name='invalid_from_address' (422) — sender domain not verified - name='not_found' (404) — segment_id does not exist - name='restricted_api_key' (401) — API key only has sending_access, not broadcast management - name='missing_required_field' (422) — name, subject, or audience missing - name='validation_error' (400/403) — template_id invalid or unpublishedRequired handlingCaller MUST check result.error after every broadcasts.create() call: const { data, error } = await resend.broadcasts.create({ name, subject, from, segmentId, html, }); if (error) { logger.error('Broadcast create failed', { name, error }); throw new Error(`Failed to create broadcast: ${error.message}`); } const broadcastId = data!.id; // Now broadcasts.send(broadcastId) can be called. Silent create failures cascade — the subsequent broadcasts.send(undefined) throws a different error (TypeError or not_found) that obscures the root cause and wastes engineering time during incident response.costmediumin prodsilent failureusers seeservice unavailablevisibilitysilent - webhooks.create · webhooks-create-no-error-checkerrorWhenwebhooks.create() called without checking result.error — silent failure on validation errors (invalid URL, unsupported events), restricted API key, or quota errors (max webhooks per team).Throws
Does not throw. Returns { data: null, error: { name, message, statusCode } } on failure. Common errors: - name='validation_error' (400) — endpoint URL invalid or not https - name='restricted_api_key' (401) — API key only has sending_access, not webhook management - name='missing_required_field' (422) — endpoint URL or events array missingRequired handlingCaller MUST check result.error AND persist data.signing_secret: const { data, error } = await resend.webhooks.create({ endpoint: 'https://my-app.com/webhooks/resend', events: ['email.delivered', 'email.bounced'], }); if (error) { throw new Error(`Failed to create webhook: ${error.message}`); } // CRITICAL: persist data.signing_secret BEFORE deploying the handler await secrets.set('RESEND_WEBHOOK_SECRET', data!.signing_secret); Silent failure here cascades to webhooks.verify() failures in production because the handler is deployed against an undefined RESEND_WEBHOOK_SECRET. See webhooks-verify-missing-secret postcondition for the downstream impact.costhighin prodsilent failureusers seeservice unavailablevisibilitysilent - events.send · events-send-no-error-checkerrorWhenevents.send() called without checking result.error — silent failure on contact not found (404), restricted API key (401), missing required field (event name / contact_id|email) (422), validation error on payload schema mismatch (400), or rate limit exceeded (429).Throws
Does not throw. Returns { data: null, error: { name, message, statusCode } } on failure. Common errors: - name='not_found' (404) — contact_id does not exist in the team's contacts - name='restricted_api_key' (401) — API key is restricted to email-only and cannot trigger automations - name='missing_required_field' (422) — event name, contact_id/email, or payload missing - name='validation_error' (400) — payload does not match the automation's defined event schema - name='rate_limit_exceeded' (429) — burst exceeded team rate limit (5 req/sec) - name='application_error' (500) — backend automation engine erroredRequired handlingCaller MUST check result.error after every events.send() call. The subscribed automations downstream are stateful (they advance the contact through an automation funnel), so a missed event silently drops the contact out of the funnel: const { data, error } = await resend.events.send({ event: 'user.signed_up', contact_id: contactId, payload: { plan: 'pro', referrer }, }); if (error) { logger.error('Automation trigger failed', { event: 'user.signed_up', contactId, error }); if (error.name === 'rate_limit_exceeded') { // Queue + retry — losing the trigger means the customer never gets // onboarding emails; this is worse than a normal email send failure // because we cannot observe the downstream impact. await queue.enqueue({ kind: 'resend.events.send', event, contactId, payload }); } else if (error.name === 'not_found') { // Contact not yet created — call contacts.create first, then retry. throw new Error(`Cannot trigger automation: contact ${contactId} not found`); } else { throw new Error(`Automation trigger failed: ${error.message}`); } } DO NOT fire-and-forget events.send(). The "no downstream evidence" property of automation triggers means silent failures are invisible for days/weeks — by the time the missing drip campaigns are noticed, the affected contacts have already churned without receiving any onboarding nurture.costhighin prodsilent failureusers seelost transactionvisibilitysilent - automations.stop · automations-stop-no-error-checkerrorWhenautomations.stop() called without checking result.error — silent failure if automation_id not found (404), API key restricted (401), automation already disabled (validation_error / 400), or backend error (500). Operator believes automation is stopped; it is not.Throws
Does not throw. Returns { data: null, error: { name, message, statusCode } } on failure. Common errors: - name='not_found' (404) — automation_id does not exist or belongs to another team - name='restricted_api_key' (401) — API key is restricted to sending and cannot manage automations - name='validation_error' (400) — automation is in a state that cannot be stopped (already disabled, archived, etc) - name='application_error' (500) — backend control-plane erroredRequired handlingCaller MUST check result.error after every automations.stop() call. Because stop is a control-plane action — the caller intends to halt customer-facing email dispatch — silent failure leaves the automation running against the operator's intent: const { data, error } = await resend.automations.stop(automationId); if (error) { logger.error('Automation stop failed', { automationId, error }); if (error.name === 'not_found') { // Automation deleted under us — confirm via automations.get() and treat as no-op throw new Error(`Automation ${automationId} not found — cannot stop`); } else if (error.name === 'validation_error') { // Already disabled — log and treat as no-op (idempotent intent) logger.warn('Automation already in stopped state', { automationId }); } else { // Backend error during incident response — page on-call immediately throw new Error(`Automation stop failed: ${error.message}`); } } else { // CRITICAL: verify data.status === 'disabled' — successful HTTP does not guarantee state change if (data!.status !== 'disabled') { throw new Error(`Automation stop returned unexpected status: ${data!.status}`); } } DO NOT call automations.stop() and assume success without inspecting both result.error AND result.data.status. During incident response, a silently-failed stop call means the offending automation continues spamming customers while the operator's runbook claims it is halted.costhighin prodsilent failureusers seeservice unavailablevisibilitysilent
Sources
Every postcondition cites at least one of these. Numbered to match the footnotes above.
- [1]resend.com/docs/api-referencehttps://resend.com/docs/api-reference/introduction#errors
- [2]resend.com/docs/api-referencehttps://resend.com/docs/api-reference/emails/send-batch-emails
- [3]resend.com/docs/api-referencehttps://resend.com/docs/api-reference/errors
- [4]resend.com/docs/api-referencehttps://resend.com/docs/api-reference/emails/send-email
- [5]resend.com/docs/api-referencehttps://resend.com/docs/api-reference/emails/update-email
- [6]resend.com/docs/api-referencehttps://resend.com/docs/api-reference/domains/verify-domain
- [7]resend.com/docs/api-referencehttps://resend.com/docs/api-reference/broadcasts/create-broadcast
- [8]resend.com/docs/api-referencehttps://resend.com/docs/api-reference/webhooks/create-webhook
- [9]resend.com/docs/api-referencehttps://resend.com/docs/api-reference/events/send-event
- [10]resend.com/docs/api-referencehttps://resend.com/docs/api-reference/automations/stop-automation
Need a different package?
Request a profile