Docs / Guides

Error Handling

notifly is designed to never crash your application due to a notification failure. This guide covers the error handling patterns available.

notify() always resolves

notify() uses Promise.allSettled internally. It always resolves to an array of results — it never rejects, even if every service call fails.

typescript
import { notify } from '@ambersecurityinc/notifly';

// This always resolves — safe to await without try/catch
const results = await notify(
  { urls: ['discord://invalid', 'ntfy://my-topic'] },
  { body: 'Hello' }
);

// Check individual results
for (const result of results) {
  if (!result.success) {
    console.error(`[${result.service}] ${result.error}`);
  }
}

NotiflyResult patterns

Check all succeeded

typescript
const results = await notify(options, message);
const allOk = results.every((r) => r.success);

Collect failures

typescript
const failures = results.filter((r) => !r.success);
if (failures.length > 0) {
  console.warn('Some notifications failed:', failures);
  // e.g. [{ success: false, service: 'discord', error: 'HTTP 401: Unauthorized' }]
}

Structured logging

typescript
const results = await notify(options, message);

for (const { success, service, error } of results) {
  if (success) {
    console.log(`[notify] ✓ ${service}`);
  } else {
    console.error(`[notify] ✗ ${service}: ${error}`);
  }
}

ParseError

Thrown by parseUrl() when a URL is malformed or uses an unknown scheme. This is the only function in notifly that throws.

typescript
import { parseUrl, ParseError } from '@ambersecurityinc/notifly';

try {
  const config = parseUrl('unknown://something');
} catch (err) {
  if (err instanceof ParseError) {
    // URL was not recognised
    console.error('Parse error:', err.message);
  } else {
    // Unexpected error — re-throw
    throw err;
  }
}

Note: You don't need to call parseUrl() directly when using notify() — it handles parsing internally and returns errors in the NotiflyResult.

ServiceError

ServiceError is thrown by a service's send() method when the upstream API returns a non-2xx response. It's caught internally by notify() and converted to a failed NotiflyResult.

typescript
class ServiceError extends Error {
  name: 'ServiceError';
  status: number;  // HTTP status code
  body: string;    // Raw response body
}

When building a custom service, you can throw ServiceError for clean error propagation:

typescript
import { ServiceError } from '@ambersecurityinc/notifly';

async function send(config, message) {
  const res = await fetch('https://api.myapp.com/notify', {
    method: 'POST',
    body: JSON.stringify({ text: message.body }),
  });

  if (!res.ok) {
    const body = await res.text();
    throw new ServiceError(
      `Upstream API error`,
      res.status,
      body
    );
  }

  return { success: true, service: 'myapp' };
}

Retry patterns

notifly doesn't retry automatically. For retry logic, wrap notify() yourself:

retry wrapper
async function notifyWithRetry(options, message, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const results = await notify(options, message);
    const failures = results.filter((r) => !r.success);

    if (failures.length === 0) return results;

    if (attempt < maxAttempts) {
      // Exponential backoff
      await new Promise((r) => setTimeout(r, 1000 * 2 ** (attempt - 1)));

      // Retry only failed URLs
      options = {
        urls: options.urls.filter((_, i) => !results[i].success),
      };
    }
  }
}

Fire-and-forget

For non-critical notifications, you may want to send without awaiting:

typescript
// Don't block on notification delivery
notify({ urls }, message).catch((err) => {
  // This only catches unexpected rejections — notify() itself never rejects
  console.error('Unexpected notify error:', err);
});
← Previous Cloudflare Workers