Guide

Observability

Typed errors, structured logging, and error reporting hooks

Errors

Every failure from Fileway is a StorageError (exported from @fileway/core) carrying a stable, machine-readable code you can branch on — check err.code, not err.message or instanceof (which doesn't survive across realms like workers/iframes):

import { StorageError } from "@fileway/core";

try {
  await client.upload(stream, { filename: "a.png" });
} catch (err) {
  if (err instanceof StorageError) {
    switch (err.code) {
      case "auth-failed": // bad/expired credentials
      case "bucket-not-found":
      case "size-exceeded":
      case "network": // fetch-level failure; safe to retry
      case "provider-error": // 5xx etc.; safe to retry
    }
  }
}

The StorageErrorCode union

CodeMeaningRetry?
validationMalformed input (ValidationError — invalid filename, size, expiresIn, escaping path, etc.)Never — fix the call
configDriver misconfiguration (e.g. missing S3 credentials, driver lacks presigned-URL support)Never — fix the config
not-foundThe object/asset does not existNo
bucket-not-foundThe S3 bucket / Cloudinary cloud does not exist or is not reachableNo
auth-failedInvalid or expired credentials, access deniedRe-auth, then retry
size-exceededPayload over the provider limit (EntityTooLarge, 413)No
networkfetch failed before an HTTP response (DNS, socket, TLS, timeout)Yes — with backoff
provider-errorThe provider returned an HTTP error without a more specific classification (e.g. 5xx)Yes — with backoff

StorageError also exposes:

  • statusCode — the HTTP status when the failure came from an HTTP response (404, 403, 500, …).
  • provider — which driver produced it: "s3", "cloudinary", or "local".
  • cause — the wrapped underlying error when one exists (e.g. the original TypeError("fetch failed") for network errors).
  • toJSON() — serializes to { name, code, message, statusCode?, provider? } for logs.
  • isAbortError(err)true when err is the standard DOMException/AbortError produced by upload cancellation (a cancellation is not a StorageError).

How codes are derived per driver

DriverClassification
LocalDriverMissing file → not-found; escaping path → validation.
S3DriverParses the AWS/MinIO XML <Code> (NoSuchBucket, NoSuchKey/NoSuchUpload, AccessDenied/InvalidAccessKeyId/SignatureDoesNotMatch/ExpiredToken/InvalidToken, EntityTooLarge) with an HTTP-status fallback (401/403 → auth-failed, 404 → not-found, 413 → size-exceeded, else provider-error). fetch failures → network; missing credentials → config.
CloudinaryDriverHTTP status mapping: 401/403 → auth-failed, upload 404 → bucket-not-found, get 404 → not-found, 413 → size-exceeded, else provider-error. fetch failures → network.

Abort signals are never wrapped: cancelling an upload rejects with a plain DOMException named AbortError (see Cancelling uploads), which isAbortError() recognizes. This is the same error fetch itself throws on abort, so your cancellation handling works uniformly.

Note on breaking changes

ValidationError is now a StorageError subclass, and driver messages are unchanged — existing try/catch, err.message, and instanceof ValidationError checks keep working. Adding the code field is additive.

Logging & Error Reporting

Attach your own loggers and error reporters as FilewayClient config — no SDK, no dependencies, no adapter package. Fileway only defines a minimal contract; you adapt your tooling with a few lines.

logger

Pass any object implementing { debug?, info?, warn?, error? } (all methods optional):

const client = new FilewayClient({
  driver,
  logger: {
    info: (message, meta) => console.log(`[fileway] ${message}`, meta),
    error: (message, meta) => console.error(`[fileway] ${message}`, meta),
  },
});

Fileway calls info with "upload started" / "upload succeeded" (and the equivalents for delete, get, getUrl, getPresignedUrl) plus structured meta ({ operation, path, filename, size, id, url, durationMs }), and error on every failure. Nothing is logged when logger is omitted. A throwing logger is caught and never breaks the operation it is reporting on.

Pino — its (obj, msg) signature is nearly compatible; adapt with a tiny wrapper:

import pino from "pino";
const pinoLogger = pino();
const client = new FilewayClient({
  driver,
  logger: {
    info: (message, meta) => pinoLogger.info(meta ?? {}, message),
    warn: (message, meta) => pinoLogger.warn(meta ?? {}, message),
    error: (message, meta) => pinoLogger.error(meta ?? {}, message),
  },
});

Datadog / any async logger — the contract methods are void, so adapt async senders by queueing or fire-and-forget (Fileway does not await logger methods):

logger: {
  info: (message, meta) => { void sendToDatadog("info", message, meta); },
  error: (message, meta) => { void sendToDatadog("error", message, meta); },
}

onError

Called (and awaited) when an operation throws — before the error is re-thrown to your caller — with the raw error and a { operation, durationMs, filename?, path?, mimeType?, size? } context. Use it to report typed failures to Sentry or your own tracker:

import * as Sentry from "@sentry/nextjs";

const client = new FilewayClient({
  driver,
  onError: (error, context) => {
    if (error instanceof StorageError) {
      Sentry.captureException(error, {
        tags: {
          fileway: "1",
          operation: context.operation,
          provider: error.provider ?? "unknown",
          code: error.code,
        },
        extra: { statusCode: error.statusCode, path: context.path },
      });
    }
  },
});

Rules:

  • Aborts are not errors. Cancelling an upload (AbortError) never fires onError.
  • ValidationError and config errors do fire — they're client bugs worth seeing.
  • A throwing onError is swallowed: it never masks the original error.
  • The error is re-thrown to your code after onError completes, so your existing error handling is unchanged.

Choosing between them

Use loggerUse onError
Structured success/start events ("upload succeeded", duration)Failures only
Local/file logging, Pino pipelinesSentry/Datadog error tracking
Fire-and-forget (not awaited)Awaited before rethrow (can flush)

Both can be set together: logger for the audit trail, onError for alerting.

On this page