@aws-sdk/client-sesv2
>=3.0.0 <4.0.0postconditions15functions12last verified2026-06-24coverage score85%Postconditions: what we check
- send · sesv2-send-no-try-catcherrorWhenAny SES v2 service error or network failure: sending quota exceeded (SendingQuotaExceededException), identity not verified (NotFoundException on unverified email), message rejected (MessageRejected), account in sandbox mode (AccountSuspendedException), permission denied, throttling (TooManyRequestsException), or network failure.Throws
SESv2ServiceException subclass with error.name set to the specific error code. For throttling: TooManyRequestsException. For invalid sender: NotFoundException or MessageRejected. For sandbox restrictions: AccountSuspendedException.Required handlingCaller MUST wrap client.send() in try-catch. SES v2 emails fail in sandbox mode (unverified recipients), on quota exhaustion, or when the sending identity is not verified. Unhandled rejections cause silent email delivery failures. Minimum handling: try { await sesv2Client.send(new SendEmailCommand({ ... })); } catch (err) { if (err instanceof SESv2ServiceException) { if (err.name === 'SendingQuotaExceededException') { // Queue for retry } console.error(`SES v2 error [${err.name}]: ${err.message}`); } throw err; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - SendEmailCommand · sesv2-send-email-no-try-catcherrorWhenAny service error or network failure: account suspended permanently (AccountSuspendedException), sending paused (SendingPausedException), sending domain not verified (MailFromDomainNotVerifiedException), message rejected for invalid content (MessageRejected), configuration set / template not found (NotFoundException), invalid parameters (BadRequestException), quota exceeded (LimitExceededException), throttling (TooManyRequestsException), or network failure.Throws
SESv2ServiceException subclass. error.name identifies the specific error code: AccountSuspendedException (account permanently blocked — not retryable), SendingPausedException (account sending disabled — check SES console), MailFromDomainNotVerifiedException (DKIM/DMARC setup incomplete), MessageRejected (virus scan failed — do not retry same content), NotFoundException (configuration set or template name does not exist), BadRequestException (malformed input — not retryable), LimitExceededException (hourly/daily send quota exceeded), TooManyRequestsException (API rate limit — retry with backoff).Required handlingCaller MUST wrap client.send(new SendEmailCommand(...)) in try-catch. SendingPausedException and AccountSuspendedException require manual intervention via SES console — not retryable by code. LimitExceededException and TooManyRequestsException should queue for delayed retry. Minimum handling: try { const result = await sesv2Client.send(new SendEmailCommand({ ... })); // result.MessageId is an acceptance receipt, NOT delivery confirmation } catch (err) { if (err instanceof SESv2ServiceException) { // AccountSuspendedException / SendingPausedException: alert ops team // MailFromDomainNotVerifiedException: check domain DKIM/DMARC setup // LimitExceededException / TooManyRequestsException: queue for retry console.error(`SES v2 SendEmail [${err.name}]: ${err.message}`); } throw err; }costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[2] - SendBulkEmailCommand · sesv2-bulk-email-result-not-checkederrorWhenSendBulkEmailCommand returns HTTP 200 with BulkEmailEntryResults array. Each entry has Status field that can be SUCCESS, ACCOUNT_SUSPENDED, ACCOUNT_THROTTLED, ACCOUNT_DAILY_QUOTA_EXCEEDED, MESSAGE_REJECTED, MAIL_FROM_DOMAIN_NOT_VERIFIED, CONFIGURATION_SET_DOES_NOT_EXIST, TEMPLATE_DOES_NOT_EXIST, ACCOUNT_SENDING_PAUSED, CONFIGURATION_SET_SENDING_PAUSED, INVALID_PARAMETER, TRANSIENT_FAILURE, or FAILED. Non-SUCCESS statuses are NOT thrown as exceptions.Throws
Nothing thrown for per-recipient failures. The HTTP 200 response contains BulkEmailEntryResults[n].Status and BulkEmailEntryResults[n].Error. Account-level errors (AccountSuspendedException, TooManyRequestsException, LimitExceededException) ARE thrown and terminate the entire request.Required handlingCaller MUST iterate over response.BulkEmailEntryResults and check each entry's Status. Recipients with non-SUCCESS status must be tracked and retried. AWS documentation explicitly states: "Check each response object and retry any messages with a failure status." Minimum handling: const response = await sesv2Client.send(new SendBulkEmailCommand({ ... })); const failed = response.BulkEmailEntryResults?.filter( r => r.Status !== 'SUCCESS' ) ?? []; if (failed.length > 0) { // Log and queue for retry failed.forEach(r => console.error(`Bulk email failed: ${r.Status} - ${r.Error}`)); }costhighin prodsilent failureusers seelost datavisibilitysilentSources[3] - SendBulkEmailCommand · sesv2-bulk-email-no-try-catcherrorWhenAccount-level errors for SendBulkEmailCommand ARE thrown as exceptions: AccountSuspendedException, SendingPausedException, TooManyRequestsException, LimitExceededException, MailFromDomainNotVerifiedException, BadRequestException. These terminate the entire bulk send operation (no entries processed).Throws
SESv2ServiceException subclass. Same error hierarchy as SendEmailCommand. AccountSuspendedException and SendingPausedException are not retryable.Required handlingCaller MUST also wrap client.send(new SendBulkEmailCommand(...)) in try-catch for account-level failures, in addition to checking per-entry BulkEmailEntryResults.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[3] - CreateEmailIdentityCommand · sesv2-create-identity-no-try-catcherrorWhenCreateEmailIdentityCommand can fail with: AlreadyExistsException (identity already registered — common in idempotent flows), LimitExceededException (AWS account identity quota reached — requires AWS support), ConcurrentModificationException (server-side conflict during concurrent identity creation), NotFoundException (invalid domain or parent resource — rare), BadRequestException (invalid email address or domain format), TooManyRequestsException (rate limit exceeded).Throws
SESv2ServiceException subclass. AlreadyExistsException (HTTP 400) is the most common: attempting to re-register an identity that already exists. This must be handled in multi-tenant flows where multiple users may register the same domain. LimitExceededException requires AWS quota increase — cannot be resolved in code. ConcurrentModificationException is a server fault (HTTP 500) — retry with backoff.Required handlingCaller MUST wrap in try-catch. AlreadyExistsException should typically be treated as success (idempotent) in registration flows. LimitExceededException requires alerting ops team — no programmatic solution. Minimum handling: try { const result = await sesv2Client.send(new CreateEmailIdentityCommand({ EmailIdentity: domain })); } catch (err) { if (err instanceof SESv2ServiceException) { if (err.name === 'AlreadyExistsException') { // Identity already exists — treat as success in idempotent flows return; } if (err.name === 'LimitExceededException') { // AWS identity quota reached — alert ops team } throw err; } throw err; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[4] - SendCustomVerificationEmailCommand · sesv2-custom-verification-no-try-catcherrorWhenSendCustomVerificationEmailCommand can fail with: NotFoundException (template name does not exist — template deleted or misspelled), MailFromDomainNotVerifiedException (sending domain unverified), MessageRejected (message content rejected — virus scan), SendingPausedException (account sending disabled), LimitExceededException (rate limit), TooManyRequestsException (API throttle), BadRequestException (invalid email address format).Throws
SESv2ServiceException subclass. NotFoundException is the most insidious error: if the custom verification template is deleted from SES while the application still references it by name, all verification emails silently fail. The error name is 'NotFoundException' with HTTP 404.Required handlingCaller MUST wrap in try-catch. NotFoundException for a missing template is a configuration error that should trigger an immediate alert — it means no verification emails can be sent until the template is recreated. Minimum handling: try { await sesv2Client.send(new SendCustomVerificationEmailCommand({ EmailAddress: userEmail, TemplateName: 'MyVerificationTemplate', })); } catch (err) { if (err instanceof SESv2ServiceException) { if (err.name === 'NotFoundException') { // Verification template deleted — alert ops, no verification emails can be sent } console.error(`Custom verification email failed [${err.name}]: ${err.message}`); } throw err; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilitysilentSources[5] - CreateEmailTemplateCommand · sesv2-create-template-no-try-catcherrorWhenCreateEmailTemplateCommand can fail with: AlreadyExistsException (template with this name already exists — not idempotent), LimitExceededException (template quota exceeded — AWS default is 20,000 templates), BadRequestException (invalid template syntax — Handlebars-style variables malformed), TooManyRequestsException (API rate limit: max 1 request/second for template operations). UpdateEmailTemplateCommand similarly throws NotFoundException when template does not exist.Throws
SESv2ServiceException subclass. AlreadyExistsException (HTTP 400) means a template with that exact name already exists. Unlike CreateEmailIdentityCommand, template creation is NOT idempotent — AlreadyExistsException means a conflict must be resolved. BadRequestException for Handlebars syntax errors in the template body.Required handlingCaller MUST wrap in try-catch. AlreadyExistsException must be handled explicitly — either use a unique name, check existence first, or use UpdateEmailTemplateCommand for existing templates. TooManyRequestsException requires throttling template operations to at most 1/second.costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[6] - CreateImportJobCommand · sesv2-import-job-result-not-polledwarningWhenCreateImportJobCommand returns a JobId immediately with HTTP 200. The import job then runs asynchronously. If the caller does not poll GetImportJobCommand for job completion/failure, it will never know whether contacts were actually imported. A failed import job silently drops all contacts.Throws
Nothing thrown for job execution failures. The API only throws for request-level errors: BadRequestException (invalid S3 path, malformed destination), LimitExceededException (concurrent import job limit exceeded), TooManyRequestsException (API rate limit).Required handlingCaller MUST poll GetImportJobCommand until JobStatus is COMPLETE or FAILED. A FAILED status with FailedRecordsS3Url indicates partial failures. Minimum handling: const { JobId } = await sesv2Client.send(new CreateImportJobCommand({ ... })); // Poll for job completion let status = 'CREATED'; while (status !== 'COMPLETED' && status !== 'FAILED') { await new Promise(r => setTimeout(r, 5000)); // 5s poll interval const job = await sesv2Client.send(new GetImportJobCommand({ JobId })); status = job.JobStatus; } if (status === 'FAILED') { // Check job.FailedRecordsS3Url for details throw new Error(`SES import job ${JobId} failed`); }costmediumin prodsilent failureusers seelost datavisibilitysilentSources[7] - CreateImportJobCommand · sesv2-import-job-no-try-catcherrorWhenCreateImportJobCommand throws on request-level failures: LimitExceededException (concurrent import job limit), BadRequestException (invalid ImportDataSource S3 path or invalid ImportDestination), TooManyRequestsException (rate limit).Throws
SESv2ServiceException subclass. LimitExceededException means too many concurrent import jobs are running — wait for existing jobs to complete before starting new ones.Required handlingCaller MUST wrap in try-catch in addition to polling job status.costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[7] - UpdateEmailTemplateCommand · sesv2-update-template-no-try-catcherrorWhenUpdateEmailTemplateCommand can fail with: NotFoundException (template with this name does not exist — must use CreateEmailTemplateCommand first), BadRequestException (invalid template syntax — malformed Handlebars variables or HTML), TooManyRequestsException (API rate limit: max 1 request/second for template operations).Throws
SESv2ServiceException subclass. NotFoundException (HTTP 404) is the primary concern: if the template was deleted externally (via console, another deploy, or cleanup script) while application code still calls UpdateEmailTemplateCommand, the update silently fails in a broad catch handler. BadRequestException for malformed Handlebars syntax in subject or body fields. TooManyRequestsException at > 1 req/sec — common in template sync jobs.Required handlingCaller MUST wrap in try-catch. NotFoundException means the template no longer exists and must be recreated with CreateEmailTemplateCommand. BadRequestException indicates a template content bug — log the template name for debugging. Minimum handling: try { await sesv2Client.send(new UpdateEmailTemplateCommand({ TemplateName: templateName, TemplateContent: { Subject: subject, Html: htmlBody }, })); } catch (err) { if (err instanceof SESv2ServiceException) { if (err.name === 'NotFoundException') { // Template deleted — recreate with CreateEmailTemplateCommand await sesv2Client.send(new CreateEmailTemplateCommand({ ... })); } if (err.name === 'TooManyRequestsException') { // Rate limit — add delay between template sync operations } console.error(`SES template update failed [${err.name}]: ${err.message}`); } throw err; }costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[8] - PutSuppressedDestinationCommand · sesv2-suppressed-destination-no-try-catcherrorWhenPutSuppressedDestinationCommand can fail with: BadRequestException (invalid EmailAddress format or invalid Reason value — must be BOUNCE or COMPLAINT), TooManyRequestsException (API rate limit exceeded — common when processing large bounce batches).Throws
SESv2ServiceException subclass. BadRequestException (HTTP 400) when Reason is not "BOUNCE" or "COMPLAINT", or when EmailAddress is malformed. TooManyRequestsException (HTTP 429) when suppression calls are batched too quickly after processing a large bounce or complaint batch from SES event webhooks.Required handlingCaller MUST wrap in try-catch. This is typically called inside bounce/complaint webhook handlers — an unhandled exception here causes the webhook to return 5xx, which triggers SES retry storms. BadRequestException should log the address for manual review. TooManyRequestsException requires backoff between batch operations. Minimum handling: try { await sesv2Client.send(new PutSuppressedDestinationCommand({ EmailAddress: bouncedAddress, Reason: 'BOUNCE', // or 'COMPLAINT' })); } catch (err) { if (err instanceof SESv2ServiceException) { if (err.name === 'TooManyRequestsException') { // Rate limited — retry with exponential backoff } console.error(`Suppression failed [${err.name}]: ${err.message}`); } throw err; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilitysilentSources[9] - CreateContactCommand · sesv2-create-contact-no-try-catcherrorWhenCreateContactCommand can fail with: AlreadyExistsException (contact with this email already exists in the contact list), NotFoundException (contact list specified in ContactListName does not exist), BadRequestException (invalid email address format or malformed TopicPreferences), TooManyRequestsException (rate limit exceeded).Throws
SESv2ServiceException subclass. AlreadyExistsException (HTTP 400) is the most common: any subscription flow where users can resubscribe will hit this. Unlike CreateEmailIdentityCommand, treating AlreadyExistsException as success is the typical correct pattern for resubscription flows. NotFoundException (HTTP 404) means the contact list was deleted — the entire subscription flow is broken until the list is recreated. BadRequestException for invalid email format (e.g. addresses not meeting RFC 5321).Required handlingCaller MUST wrap in try-catch. AlreadyExistsException should typically update the contact's subscription status using UpdateContactCommand rather than failing silently. NotFoundException requires immediate alerting — all new subscriptions are failing. Minimum handling: try { await sesv2Client.send(new CreateContactCommand({ ContactListName: 'MyNewsletter', EmailAddress: userEmail, TopicPreferences: [{ TopicName: 'updates', SubscriptionStatus: 'OPT_IN' }], })); } catch (err) { if (err instanceof SESv2ServiceException) { if (err.name === 'AlreadyExistsException') { // Contact exists — update subscription preferences instead await sesv2Client.send(new UpdateContactCommand({ ... })); return; } if (err.name === 'NotFoundException') { // Contact list missing — alert ops team } console.error(`SES contact creation failed [${err.name}]: ${err.message}`); } throw err; }costmediumin prodimmediate exceptionusers seelost datavisibilitysilentSources[10] - UpdateContactCommand · sesv2-update-contact-no-try-catcherrorWhenUpdateContactCommand can fail with: NotFoundException (contact list does not exist OR contact with this email not found in list), BadRequestException (invalid TopicPreferences shape, invalid SubscriptionStatus value, or malformed email), ConcurrentModificationException (server-side conflict during concurrent contact updates — HTTP 500), TooManyRequestsException (API rate limit exceeded — common during bulk preference-sync jobs).Throws
SESv2ServiceException subclass. NotFoundException (HTTP 404) is the most common failure mode: it conflates "contact list missing" with "contact missing in list", so error handling must read error.message to distinguish them. In webhook handlers processing preference-change events, a NotFoundException silently drops the preference update unless caught. ConcurrentModificationException is a server fault retryable with exponential backoff.Required handlingCaller MUST wrap client.send(new UpdateContactCommand(...)) in try-catch. NotFoundException requires distinguishing list-missing (ops alert) from contact-missing (typically: fall back to CreateContactCommand to upsert the contact). ConcurrentModificationException should retry with backoff. Minimum handling: try { await sesv2Client.send(new UpdateContactCommand({ ContactListName: 'MyNewsletter', EmailAddress: userEmail, TopicPreferences: allDesiredTopics, // MUST include all topics, not just changed ones })); } catch (err) { if (err instanceof SESv2ServiceException) { if (err.name === 'NotFoundException') { // Distinguish list-missing vs contact-missing via err.message // For contact-missing: fall back to CreateContactCommand (upsert) await sesv2Client.send(new CreateContactCommand({ ... })); return; } if (err.name === 'ConcurrentModificationException') { // Server-side conflict — retry with exponential backoff } console.error(`SES contact update failed [${err.name}]: ${err.message}`); } throw err; }costmediumin prodimmediate exceptionusers seelost datavisibilitysilentSources[11] - CreateExportJobCommand · sesv2-export-job-result-not-polledwarningWhenCreateExportJobCommand returns a JobId immediately with HTTP 200. The export job then runs asynchronously. If the caller does not poll GetExportJobCommand for COMPLETED / FAILED / CANCELLED status, it will never know whether the export actually produced output. A FAILED export job silently drops all requested data, and reporting / analytics pipelines downstream see an empty S3 bucket they assume contains complete data.Throws
Nothing thrown for job execution failures. The API only throws for request-level errors: BadRequestException (invalid ExportDataSource or ExportDestination S3 URL), LimitExceededException (concurrent export job limit exceeded — AWS account quota), NotFoundException (referenced configuration set or message-insights filter target does not exist), TooManyRequestsException (API rate limit > 1 req/sec).Required handlingCaller MUST poll GetExportJobCommand until JobStatus is COMPLETED, FAILED, or CANCELLED. A FAILED status with FailureInfo indicates the export did not produce the expected S3 output — downstream analytics MUST be paused until the failure is investigated. Minimum handling: const { JobId } = await sesv2Client.send(new CreateExportJobCommand({ ... })); // Poll for job completion let status = 'CREATED'; while (status !== 'COMPLETED' && status !== 'FAILED' && status !== 'CANCELLED') { await new Promise(r => setTimeout(r, 5000)); // 5s poll interval const job = await sesv2Client.send(new GetExportJobCommand({ JobId })); status = job.JobStatus; } if (status !== 'COMPLETED') { // Check job.FailureInfo for details; downstream analytics must NOT consume S3 output throw new Error(`SES export job ${JobId} ended with status ${status}`); }costmediumin prodsilent failureusers seelost datavisibilitysilentSources[12] - CreateExportJobCommand · sesv2-export-job-no-try-catcherrorWhenCreateExportJobCommand throws on request-level failures: LimitExceededException (concurrent export job limit reached — must wait for existing jobs to finish), BadRequestException (invalid ExportDataSource shape or invalid S3Url destination), NotFoundException (referenced resource missing), TooManyRequestsException (API rate limit > 1 req/sec, common in scheduled export jobs that fire on a cron).Throws
SESv2ServiceException subclass. LimitExceededException means too many concurrent export jobs are running — caller must wait for existing jobs to complete before starting new ones. TooManyRequestsException requires 1-second minimum spacing between calls.Required handlingCaller MUST wrap client.send(new CreateExportJobCommand(...)) in try-catch in addition to polling job status. LimitExceededException should defer the export and retry later (not immediately). BadRequestException indicates a programming error — log the request shape for debugging.costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[12]
Sources
Every postcondition cites at least one of these. Grouped by source type; numbered to match the footnotes above.
- [1]docs.aws.amazon.com/AWSJavaScriptSDK/v3/latestSesv2
- [2]docs.aws.amazon.com/ses/latest/APIReference-V2API SendEmail
- [3]docs.aws.amazon.com/ses/latest/APIReference-V2API SendBulkEmail
- [4]docs.aws.amazon.com/ses/latest/APIReference-V2API CreateEmailIdentity
- [5]docs.aws.amazon.com/ses/latest/APIReference-V2API SendCustomVerificationEmail
- [6]docs.aws.amazon.com/ses/latest/APIReference-V2API CreateEmailTemplate
- [7]docs.aws.amazon.com/ses/latest/APIReference-V2API CreateImportJob
- [8]docs.aws.amazon.com/ses/latest/APIReference-V2API UpdateEmailTemplate
- [9]docs.aws.amazon.com/ses/latest/APIReference-V2API PutSuppressedDestination
- [10]docs.aws.amazon.com/ses/latest/APIReference-V2API CreateContact
- [11]docs.aws.amazon.com/ses/latest/APIReference-V2API UpdateContact
- [12]docs.aws.amazon.com/ses/latest/APIReference-V2API CreateExportJob
Research notes
Curator notes from SOURCES.md captured when the profile was written so you can verify the reasoning, not just the rules.
Sources: @aws-sdk/client-sesv2
Why SESv2Client.send() Requires Error Handling
Official AWS Documentation
- SDK v3 SESv2 Client: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/sesv2/
- SendEmail API Reference: https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendEmail.html
- SES v2 Error Codes: https://docs.aws.amazon.com/ses/latest/APIReference-V2/CommonErrors.html
- Sandbox Mode: https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html
Key Behavioral Differences from SES v1
SESv2 is a distinct API from SES v1 with different:
- Parameter names:
FromEmailAddress(notSource),Content.Simple(notMessage) - SDK classes:
SESv2ClientandSESv2ServiceException(notSESClient/SESServiceException) - Import path:
@aws-sdk/client-sesv2(separate npm package) - Error codes:
SendingQuotaExceededException(notLimitExceededException)
Error Conditions That Require Handling
-
Sandbox restrictions (most common in development): New AWS accounts are placed in sandbox mode where only verified email addresses can receive emails. Any send to an unverified recipient throws immediately.
-
Sending quota exceeded (
SendingQuotaExceededException): AWS enforces per-second and per-day sending limits. SaaS apps sending transactional email at scale will hit these. -
Unverified sending identity (
NotFoundException): TheFromEmailAddressdomain or address must be verified in SES. Misconfigured production environments will fail here. -
Message rejected (
MessageRejected): Content detected as spam or policy violation. -
Account suspended (
AccountSuspendedException): Account-level suspension (e.g., high bounce rates triggering AWS enforcement action). -
Throttling (
TooManyRequestsException): Request rate exceeded — retryable with backoff. -
Network failures: Standard network-layer failures (timeout, DNS, connection reset).
Evidence of Runtime Failures
These errors are not theoretical — they are the most common SES-related support questions:
- Sandbox mode blocks emails silently without try-catch
- Sending quota exhaustion causes partial email delivery in bulk campaigns
- Identity verification failures are common when deploying to new AWS accounts/regions
Contract Scope
This contract covers SESv2Client.send() as the single entry point for all SESv2 API calls.
All operations (email sending, identity management, contact management) go through this method.