mailgun.js
semver
>=3.0.0postconditions24functions16last verified2026-04-16coverage score100%Postconditions — what we check
- create · mailgun-api-errorerrorWhenAny HTTP error or network failure: authentication failure (401), domain not found (404), rate limit exceeded (429), malformed request (400), service unavailable (5xx), or any network-level failure (DNS, timeout, connection refused).Throws
Error object with status code and details. For API errors: { status: 401|404|400|429|5xx, details: string }. For network errors: generic Error with connection message. Example: [Error: UNAUTHORIZED] { status: 401, details: 'Forbidden' }Required handlingCaller MUST wrap mg.messages.create() in try-catch. Email delivery failures are common in production (invalid API key, domain misconfiguration, rate limits). Unhandled promise rejections will crash the Node.js process or silently lose emails. Minimum handling: try { const result = await mg.messages.create(domain, messageData); return result; } catch (error) { logger.error('Email delivery failed', { error }); throw error; // or handle gracefully }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - validate.get · validate-authentication-errorerrorWhenAPI key is missing, invalid, or revoked. HTTP 401 response from Mailgun. Also occurs when using a private API key for a restricted endpoint.Throws
APIError with { status: 401, message: 'Forbidden', type: 'MailgunAPIError' }. Check via: err?.type === 'MailgunAPIError' && err.status === 401.Required handlingCaller MUST wrap mg.validate.get() in try-catch. Authentication failures in signup flows silently pass unvalidated emails downstream or crash the signup endpoint. Minimum handling: try { const result = await mg.validate.get(email); return result; } catch (error) { if (error?.type === 'MailgunAPIError') { logger.error('Email validation failed', { status: error.status, details: error.details }); } throw error; }costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - validate.get · validate-quota-exceededwarningWhenEmail validation quota exhausted (HTTP 402 PaymentRequired or 429 TooManyRequests). Mailgun enforces per-plan validation quotas; exceeding them causes all subsequent validate.get() calls to fail for the billing period or until the rate window resets.Throws
APIError with { status: 402, type: 'MailgunAPIError' } for quota exhaustion, or { status: 429 } for rate limiting. Check via err?.type === 'MailgunAPIError'.Required handlingQuota exhaustion must be caught and handled gracefully — treat validation as optional when quota is exceeded rather than blocking registration entirely. Minimum handling: try { const result = await mg.validate.get(email); return result; } catch (error) { if (error?.type === 'MailgunAPIError' && (error.status === 402 || error.status === 429)) { // Degrade gracefully — allow signup to proceed without validation return { is_valid: true, degraded: true }; } throw error; }costlowin prodimmediate exceptionusers seedegraded performancevisibilitysilentSources[4] - suppressions.create · suppressions-create-auth-errorerrorWhenAPI key is invalid or missing. HTTP 401 from Mailgun. Also occurs when the API key lacks permission for the domain.Throws
APIError with { status: 401, type: 'MailgunAPIError' }.Required handlingCaller MUST wrap mg.suppressions.create() in try-catch. Suppression failures are a compliance risk — if an unsubscribe cannot be recorded, subsequent sends to that address may violate CAN-SPAM/GDPR and trigger spam complaints. Minimum handling: try { await mg.suppressions.create(domain, 'unsubscribes', { address: email }); } catch (error) { if (error?.type === 'MailgunAPIError') { logger.error('Failed to add suppression', { status: error.status, email }); // Must retry or alert — failing to record unsubscribe is a compliance violation } throw error; }costhighin prodimmediate exceptionusers seedegraded performancevisibilitysilent - suppressions.create · suppressions-create-domain-not-founderrorWhenDomain passed to suppressions.create() does not exist in the Mailgun account. HTTP 404 from Mailgun. Common when using the wrong domain string or a domain that has been removed from the account.Throws
APIError with { status: 404, type: 'MailgunAPIError' }.Required handlingLog and alert on 404 — it indicates a misconfiguration. The domain should be verified before attempting to add suppressions.costmediumin prodimmediate exceptionusers seedegraded performancevisibilitysilentSources[4] - suppressions.upload · suppressions-upload-auth-errorerrorWhenAPI key is invalid or missing. HTTP 401 from Mailgun.Throws
APIError with { status: 401, type: 'MailgunAPIError' }.Required handlingCaller MUST wrap mg.suppressions.upload() in try-catch. Bulk suppression import failures should be logged and the operation retried or flagged for manual review to avoid compliance violations. Minimum handling: try { await mg.suppressions.upload(domain, 'bounces', fileData); } catch (error) { if (error?.type === 'MailgunAPIError') { logger.error('Suppression upload failed', { status: error.status }); } throw error; }costmediumin prodimmediate exceptionusers seedegraded performancevisibilitysilentSources[5] - suppressions.upload · suppressions-upload-invalid-formatwarningWhenFile data is malformed, wrong MIME type, or the file content does not match the expected format for the suppression type. HTTP 400 from Mailgun.Throws
APIError with { status: 400, type: 'MailgunAPIError', details: string }.Required handlingValidate file format before uploading. On 400 errors, log the details field for debugging — it typically contains the format validation message.costlowin prodimmediate exceptionusers seedegraded performancevisibilitysilentSources[5] - validate.multipleValidation.create · bulk-validate-create-auth-errorerrorWhenAPI key is invalid, or the plan does not include bulk validation features. HTTP 401 or 403 from Mailgun.Throws
APIError with { status: 401 | 403, type: 'MailgunAPIError' }.Required handlingCaller MUST wrap in try-catch. Bulk validation job creation failures should be handled by retrying with exponential backoff or alerting the operator. Minimum handling: try { const job = await mg.validate.multipleValidation.create(listId, data); return job; } catch (error) { if (error?.type === 'MailgunAPIError') { logger.error('Bulk validation create failed', { status: error.status }); } throw error; }costlowin prodimmediate exceptionusers seedegraded performancevisibilitysilentSources[4] - validate.multipleValidation.create · bulk-validate-create-quota-exceededwarningWhenBulk validation quota exhausted for the plan. HTTP 402 or 429 from Mailgun. Different from single-address validation quota.Throws
APIError with { status: 402 | 429, type: 'MailgunAPIError' }.Required handlingMust handle quota exhaustion gracefully — log the error and defer the batch job rather than retrying immediately.costlowin prodimmediate exceptionusers seedegraded performancevisibilitysilentSources[4] - lists.create · lists-create-auth-errorerrorWhenAPI key is invalid or missing. HTTP 401 from Mailgun.Throws
APIError with { status: 401, type: 'MailgunAPIError' }.Required handlingCaller MUST wrap mg.lists.create() in try-catch. Minimum handling: try { const list = await mg.lists.create({ address: 'news@example.com' }); return list; } catch (error) { if (error?.type === 'MailgunAPIError') { logger.error('Mailing list create failed', { status: error.status }); } throw error; }costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[4] - lists.create · lists-create-duplicate-addresswarningWhenA mailing list with the same address already exists. HTTP 400 from Mailgun with message indicating the list already exists.Throws
APIError with { status: 400, type: 'MailgunAPIError', details: 'Duplicate list address' }.Required handlingCheck for existing list before creating, or handle the 400 error by treating it as a no-op (idempotent creation).costlowin prodimmediate exceptionusers seedegraded performancevisibilitysilentSources[4] - lists.members.createMember · list-member-create-auth-errorerrorWhenAPI key is invalid or missing. HTTP 401 from Mailgun.Throws
APIError with { status: 401, type: 'MailgunAPIError' }.Required handlingCaller MUST wrap mg.lists.members.createMember() in try-catch. Subscription failures must be logged — a silent failure means the user believes they subscribed but never receives any emails. Minimum handling: try { await mg.lists.members.createMember(listAddress, { address: email }); } catch (error) { if (error?.type === 'MailgunAPIError') { logger.error('List member add failed', { status: error.status }); } throw error; }costlowin prodimmediate exceptionusers seelost datavisibilitysilentSources[4] - lists.members.createMember · list-member-create-list-not-founderrorWhenThe mailing list address does not exist. HTTP 404 from Mailgun. Common when using incorrect list address or after a list has been deleted.Throws
APIError with { status: 404, type: 'MailgunAPIError' }.Required handlingLog 404 and alert — indicates list misconfiguration. Should not retry without creating the list first.costlowin prodimmediate exceptionusers seelost datavisibilitysilentSources[4] - lists.members.createMembers · bulk-member-create-auth-errorerrorWhenAPI key is invalid or missing. HTTP 401 from Mailgun.Throws
APIError with { status: 401, type: 'MailgunAPIError' }.Required handlingCaller MUST wrap mg.lists.members.createMembers() in try-catch. Bulk import failures should trigger a retry mechanism or alert the operator. Minimum handling: try { await mg.lists.members.createMembers(listAddress, { members, upsert: true }); } catch (error) { if (error?.type === 'MailgunAPIError') { logger.error('Bulk member import failed', { status: error.status, count: members.length }); } throw error; }costmediumin prodimmediate exceptionusers seelost datavisibilitysilentSources[4] - lists.members.createMembers · bulk-member-create-payload-too-largewarningWhenMembers array is too large for a single request. HTTP 400 or 413 from Mailgun. Mailgun has per-request limits for bulk member imports.Throws
APIError with { status: 400 | 413, type: 'MailgunAPIError' }.Required handlingChunk large imports into batches of 1000 members or fewer. Handle the error by splitting the members array and retrying each chunk.costlowin prodimmediate exceptionusers seelost datavisibilitysilentSources[4] - suppressions.destroy · suppressions-destroy-no-try-catcherrorWhenmg.suppressions.destroy() is called without a surrounding try-catch block. Any HTTP error or network failure — authentication failure (401), domain not found (404), address not found in list (404), or connection errors — will cause an unhandled promise rejection.Throws
APIError with { status: 401|404|5xx, type: 'MailgunAPIError', details: string }. For network errors: generic Error with connection message.Required handlingCaller MUST wrap mg.suppressions.destroy() in try-catch. Suppression removal is a compliance operation — if the error is silently swallowed, the address remains suppressed and the user is never re-enabled, silently breaking the re-subscribe or false-bounce-correction workflow. Minimum handling: try { const result = await mg.suppressions.destroy(domain, 'unsubscribes', address); return result; // { message, address, status: 200 } } catch (error) { if (error?.type === 'MailgunAPIError') { if (error.status === 404) { // Address not in list — treat as success (idempotent) return null; } logger.error('Failed to remove suppression', { status: error.status, address }); } throw error; }costlowin prodimmediate exceptionusers seelost datavisibilitysilent - suppressions.destroyAll · suppressions-destroy-all-no-try-catcherrorWhenmg.suppressions.destroyAll() is called without a surrounding try-catch block. Authentication failure (401), domain not found (404), or network errors cause an unhandled promise rejection.Throws
APIError with { status: 401|404|5xx, type: 'MailgunAPIError', details: string }. For network errors: generic Error with connection message.Required handlingCaller MUST wrap mg.suppressions.destroyAll() in try-catch. This is a destructive bulk operation — unhandled errors mean the caller cannot confirm whether the bulk deletion succeeded or partially failed. Minimum handling: try { const result = await mg.suppressions.destroyAll(domain, 'bounces'); logger.info('All bounce suppressions cleared', result); } catch (error) { if (error?.type === 'MailgunAPIError') { logger.error('Failed to clear all suppressions', { status: error.status, domain }); } throw error; }costhighin prodimmediate exceptionusers seelost datavisibilitysilent - suppressions.destroyAll · suppressions-destroy-all-catastrophic-misusewarningWhenmg.suppressions.destroyAll() is called without explicit operator confirmation or safeguard. Unlike suppressions.destroy (single-address removal), this call irreversibly deletes ALL suppression records of the given type for the entire domain. There is no undo. A single miscall can re-enable thousands of previously bounced/unsubscribed addresses, causing mass resend to dead addresses (bounce rate spike) or opted-out users (CAN-SPAM violation).Throws
Does NOT throw on success — the danger is that success is silent. The operation completes and returns { message: '...addresses for this domain have been removed', status: 200 }. The risk is calling this method at all without proper safeguards.Required handlingCallers MUST require explicit confirmation before calling destroyAll. This is not a routine operation — it should be restricted to admin-only interfaces with a confirmation dialog. Never call in automated workflows without a human-in-the-loop check. Minimum safeguard: // Always confirm intent before calling if (!explicitAdminConfirmation) { throw new Error('destroyAll requires explicit admin confirmation'); } try { const result = await mg.suppressions.destroyAll(domain, type); logger.warn('BULK suppression deletion completed', { domain, type, result }); } catch (error) { throw error; }costhighin prodsilent failureusers seelost datavisibilitysilent - lists.update · lists-update-no-try-catcherrorWhenmg.lists.update() is called without a surrounding try-catch block. Authentication failure (401), list not found (404), invalid address format (400), or network errors cause an unhandled promise rejection.Throws
APIError with { status: 401|404|400|5xx, type: 'MailgunAPIError', details: string }.Required handlingCaller MUST wrap mg.lists.update() in try-catch. Mailing list update failures should be surfaced to the user — silently swallowing errors means the update appeared to succeed but the list was not actually modified. Minimum handling: try { const updated = await mg.lists.update(listAddress, { name: newName }); return updated; } catch (error) { if (error?.type === 'MailgunAPIError') { if (error.status === 404) { throw new Error('Mailing list not found'); } logger.error('List update failed', { status: error.status }); } throw error; }costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - lists.destroy · lists-destroy-no-try-catcherrorWhenmg.lists.destroy() is called without a surrounding try-catch block. Authentication failure (401), list not found (404), or network errors cause an unhandled promise rejection.Throws
APIError with { status: 401|404|5xx, type: 'MailgunAPIError', details: string }.Required handlingCaller MUST wrap mg.lists.destroy() in try-catch. Mailing list deletion errors should be caught and logged — a 404 should be treated as a success (idempotent deletion), while other errors indicate a real failure. Minimum handling: try { const result = await mg.lists.destroy(listAddress); return result; // { address, message: 'Mailing list has been removed' } } catch (error) { if (error?.type === 'MailgunAPIError') { if (error.status === 404) { return null; // Already deleted — treat as success } logger.error('List deletion failed', { status: error.status, listAddress }); } throw error; }costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - lists.members.updateMember · list-member-update-no-try-catcherrorWhenmg.lists.members.updateMember() is called without a surrounding try-catch block. This is especially risky when used to set subscribed: false — a failed unsubscribe silently continues delivering to an opted-out user, which is a CAN-SPAM compliance violation.Throws
APIError with { status: 401|404|400|5xx, type: 'MailgunAPIError', details: string }.Required handlingCaller MUST wrap mg.lists.members.updateMember() in try-catch. Particularly when setting subscribed: false, errors must NOT be silently swallowed — a failed unsubscribe means the user continues receiving mail despite opting out. Minimum handling: try { const member = await mg.lists.members.updateMember(listAddress, email, { subscribed: false }); return member; } catch (error) { if (error?.type === 'MailgunAPIError') { if (error.status === 404) { // Member doesn't exist — unsubscribe is effectively a no-op return null; } logger.error('Member update failed', { status: error.status, email }); // Must retry or alert — failing to record unsubscribe is a compliance risk } throw error; }costmediumin prodimmediate exceptionusers seedegraded performancevisibilitysilent - lists.members.destroyMember · list-member-destroy-no-try-catcherrorWhenmg.lists.members.destroyMember() is called without a surrounding try-catch block. This is high-risk when used in GDPR deletion workflows or unsubscribe flows — a silently swallowed error means the member remains on the list and continues receiving mail despite requesting deletion.Throws
APIError with { status: 401|404|5xx, type: 'MailgunAPIError', details: string }.Required handlingCaller MUST wrap mg.lists.members.destroyMember() in try-catch. In GDPR/unsubscribe flows, errors MUST be logged and the operation must be retried or escalated — failure to remove a member who requested deletion is a compliance violation. Minimum handling: try { const result = await mg.lists.members.destroyMember(listAddress, memberAddress); return result; // { member: { address }, message: 'Mailing list member has been deleted' } } catch (error) { if (error?.type === 'MailgunAPIError') { if (error.status === 404) { // Member already removed — treat as success (idempotent) return null; } logger.error('Member deletion failed', { status: error.status, memberAddress }); // Must retry or alert for compliance workflows } throw error; }costmediumin prodimmediate exceptionusers seelost datavisibilitysilent - webhooks.create · webhooks-create-no-try-catcherrorWhenmg.webhooks.create() is called without a surrounding try-catch block. Authentication failure (401), domain not found (404), invalid event type, or URL validation failure cause an unhandled promise rejection. If the webhook creation fails silently, events are never delivered to the registered URL — the application loses all email event notifications without alerting operators.Throws
APIError with { status: 401|404|400|5xx, type: 'MailgunAPIError', details: string }.Required handlingCaller MUST wrap mg.webhooks.create() in try-catch. Webhook creation is typically done during application setup — a silent failure means the entire event notification pipeline is broken from day one. Minimum handling: try { const webhook = await mg.webhooks.create(domain, 'bounce', 'https://app.example.com/webhooks/mailgun'); logger.info('Webhook registered', { event: 'bounce', domain }); } catch (error) { if (error?.type === 'MailgunAPIError') { logger.error('Webhook creation failed', { status: error.status, domain }); throw new Error(`Failed to register Mailgun webhook: ${error.status}`); } throw error; }costlowin prodimmediate exceptionusers seedegraded performancevisibilitysilentSources[6] - webhooks.update · webhooks-update-no-try-catcherrorWhenmg.webhooks.update() is called without a surrounding try-catch block. Authentication failure (401) or webhook not found (404) cause an unhandled promise rejection. A silently failed update means the webhook continues pointing at the old (possibly offline) URL, causing silent event delivery failures.Throws
APIError with { status: 401|404|5xx, type: 'MailgunAPIError', details: string }.Required handlingCaller MUST wrap mg.webhooks.update() in try-catch. Webhook URL rotation is a critical infrastructure operation — failures must be surfaced to prevent silent event loss. Minimum handling: try { const result = await mg.webhooks.update(domain, 'bounce', 'https://new.example.com/webhooks'); logger.info('Webhook updated', { event: 'bounce' }); } catch (error) { if (error?.type === 'MailgunAPIError') { logger.error('Webhook update failed', { status: error.status }); } throw error; }costlowin prodimmediate exceptionusers seedegraded performancevisibilitysilent
Sources
Every postcondition cites at least one of these. Numbered to match the footnotes above.
- [1]github.com/mailgun/mailgun.jshttps://github.com/mailgun/mailgun.js#readme
- [2]github.com/mailgun/mailgun.jshttps://github.com/mailgun/mailgun.js/issues/411
- [3]documentation.mailgun.com/docs/mailgunhttps://documentation.mailgun.com/docs/mailgun/api-reference/openapi-final/tag/Messages/
- [4]github.com/mailgun/mailgun.jshttps://github.com/mailgun/mailgun.js/blob/v10.2.3/README.md
- [5]github.com/mailgun/mailgun.jshttps://github.com/mailgun/mailgun.js/blob/main/CHANGELOG.md
- [6]github.com/mailgun/mailgun.jshttps://github.com/mailgun/mailgun.js/blob/main/README.md
- [7]documentation.mailgun.com/docs/mailgunhttps://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/bounces/delete-v3--domainid--bounces--address-
- [8]documentation.mailgun.com/docs/mailgunhttps://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/unsubscribe/delete-v3--domainid--unsubscribes--address-
- [9]documentation.mailgun.com/docs/mailgunhttps://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/bounces/delete-v3--domainid--bounces
- [10]documentation.mailgun.com/docs/mailgunhttps://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/mailing-lists/put-v3-lists-address
- [11]documentation.mailgun.com/docs/mailgunhttps://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/mailing-lists/delete-v3-lists-address
- [12]documentation.mailgun.com/docs/mailgunhttps://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/mailing-lists/put-lists-list_address-members-member_address
- [13]documentation.mailgun.com/docs/mailgunhttps://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/mailing-lists/delete-lists-list_address-members-member_address
- [14]documentation.mailgun.com/docs/inboxreadyhttps://documentation.mailgun.com/docs/inboxready/api-reference/optimize/mailgun/webhooks/put-v3-domains--domain-name--webhooks--webhook-name-
Need a different package?
Request a profile