</>
APIGuiden.se

Foto av Max Duzij via Unsplash

REST11 min2026-04-09

API Felhantering: Bygg robusta integrationer med korrekt error handling

Lär dig designa konsekventa felmeddelanden, hantera HTTP-felkoder och implementera retry-logik. Komplett guide till professionell API error handling.

FelhanteringError HandlingRESTRobusthet

Varför felhantering avgör API-kvaliteten

Ett API:s kvalitet avslöjas inte i de lyckade anropen utan i hur det hanterar fel. Tydliga, konsekventa felmeddelanden gör skillnaden mellan en integration som tar en timme att bygga och en som tar en vecka. Dålig felhantering leder till frustration, supportärenden och klienter som slutar använda ditt API.

Felhantering har två sidor: hur ditt API kommunicerar fel till klienten, och hur din klient hanterar fel från externa API:er. Båda är lika viktiga. Ett API som returnerar generiska 500-fel utan kontext är lika problematiskt som en klient som kraschar vid första bästa timeout.

Grunden är att använda HTTP-statuskoder korrekt och konsekvent, komplettera med strukturerade felmeddelanden i response body, och implementera retry-logik med exponential backoff för transienta fel.

error-response.json
json
// Standardiserat felformat (RFC 7807 - Problem Details)
{
  "type": "https://api.example.com/errors/validation-error",
  "title": "Valideringsfel",
  "status": 422,
  "detail": "Fältet 'email' har ogiltigt format",
  "instance": "/api/v1/users",
  "errors": [
    {
      "field": "email",
      "code": "invalid_format",
      "message": "Ange en giltig e-postadress"
    },
    {
      "field": "age",
      "code": "out_of_range",
      "message": "Åldern måste vara mellan 0 och 150"
    }
  ],
  "requestId": "req_a1b2c3d4",
  "timestamp": "2026-04-09T10:30:00Z"
}

HTTP-statuskoder för felhantering

Varje HTTP-statuskod har en specifik betydelse. 4xx-koder indikerar att klienten gjort något fel och behöver ändra sin request. 5xx-koder signalerar att servern har problem och klienten kan försöka igen senare.

De viktigaste felkoderna att använda korrekt: 400 Bad Request (request kunde inte parsas), 401 Unauthorized (ingen autentisering), 403 Forbidden (autentiserad men saknar behörighet), 404 Not Found (resursen finns inte), 409 Conflict (resurskollision, t.ex. duplicerat e-post), 422 Unprocessable Entity (request parsades men misslyckades med validering), 429 Too Many Requests (rate limit överskriden).

På serversidan: 500 Internal Server Error (oväntat fel), 502 Bad Gateway (upstream-tjänst svarar inte), 503 Service Unavailable (tillfälligt nere, inkludera Retry-After header) och 504 Gateway Timeout (upstream timeout).

En vanlig fälla är att returnera 200 med ett felmeddande i bodyn. Det bryter HTTP-kontraktet och gör det omöjligt för mellanlager (cacher, load balancers, monitoring) att tolka resultatet korrekt.

lib/errors.ts
typescript
// Express.js: Centraliserad felhantering
class ApiError extends Error {
  constructor(
    public statusCode: number,
    public code: string,
    message: string,
    public details?: Record<string, unknown>
  ) {
    super(message);
    this.name = "ApiError";
  }

  // Factory methods för vanliga fel
  static badRequest(message: string, details?: Record<string, unknown>) {
    return new ApiError(400, "bad_request", message, details);
  }

  static unauthorized(message = "Autentisering krävs") {
    return new ApiError(401, "unauthorized", message);
  }

  static forbidden(message = "Åtkomst nekad") {
    return new ApiError(403, "forbidden", message);
  }

  static notFound(resource: string) {
    return new ApiError(404, "not_found", `${resource} hittades inte`);
  }

  static conflict(message: string) {
    return new ApiError(409, "conflict", message);
  }

  static validationError(errors: Array<{ field: string; message: string }>) {
    return new ApiError(422, "validation_error", "Valideringsfel", {
      errors,
    });
  }

  static tooManyRequests(retryAfter: number) {
    return new ApiError(429, "rate_limit_exceeded", "För många anrop", {
      retryAfter,
    });
  }
}

// Global error handler middleware
function errorHandler(err, req, res, _next) {
  // Generera unikt request-ID för spårning
  const requestId =
    req.headers["x-request-id"] || crypto.randomUUID();

  if (err instanceof ApiError) {
    return res.status(err.statusCode).json({
      error: err.code,
      message: err.message,
      ...err.details,
      requestId,
    });
  }

  // Okänt fel - logga och returnera generiskt svar
  console.error(`[${requestId}] Unhandled error:`, err);
  res.status(500).json({
    error: "internal_error",
    message: "Ett oväntat fel uppstod",
    requestId,
  });
}

Retry-logik med exponential backoff

Transienta fel (5xx, timeouts, nätverksfel) löser sig ofta av sig själva. Implementera retry-logik med exponential backoff: första retry efter 1 sekund, andra efter 2 sekunder, tredje efter 4 sekunder. Lägg till jitter (slumpmässig fördröjning) för att undvika att alla klienter retryar simultant.

Aldrig retrya 4xx-fel. Om servern säger att din request är ogiltig hjälper det inte att skicka samma request igen. Undantaget är 429 Too Many Requests, där du väntar den tid som Retry-After-headern anger.

Sätt alltid en maxgräns för antal retries. Tre till fem försök räcker i de flesta fall. Logga varje retry med request-ID för felsökning.

lib/http-client.ts
typescript
// Retry med exponential backoff och jitter
interface RetryConfig {
  maxRetries: number;
  baseDelay: number;   // Millisekunder
  maxDelay: number;
  retryableStatuses: number[];
}

const DEFAULT_CONFIG: RetryConfig = {
  maxRetries: 3,
  baseDelay: 1000,
  maxDelay: 30000,
  retryableStatuses: [408, 429, 500, 502, 503, 504],
};

async function fetchWithRetry(
  url: string,
  options: RequestInit = {},
  config: RetryConfig = DEFAULT_CONFIG
): Promise<Response> {
  let lastError: Error | null = null;

  for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
    try {
      const response = await fetch(url, {
        ...options,
        signal: AbortSignal.timeout(10000), // 10s timeout
      });

      // Lyckat svar eller icke-retryable fel
      if (
        response.ok ||
        !config.retryableStatuses.includes(response.status)
      ) {
        return response;
      }

      // Respektera Retry-After header
      const retryAfter = response.headers.get("Retry-After");
      if (retryAfter && attempt < config.maxRetries) {
        const delay = parseInt(retryAfter, 10) * 1000 || config.baseDelay;
        await sleep(delay);
        continue;
      }

      lastError = new Error(`HTTP ${response.status}`);
    } catch (err) {
      lastError = err as Error;
    }

    // Exponential backoff med jitter
    if (attempt < config.maxRetries) {
      const delay = Math.min(
        config.baseDelay * Math.pow(2, attempt),
        config.maxDelay
      );
      const jitter = delay * 0.5 * Math.random();
      await sleep(delay + jitter);
    }
  }

  throw new Error(
    `Misslyckades efter ${config.maxRetries + 1} försök: ${lastError?.message}`
  );
}

function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

Circuit breaker-mönstret

När en extern tjänst är helt nere vill du inte bombardera den med retries. Circuit breaker-mönstret stoppar anrop efter ett antal misslyckanden och låter tjänsten återhämta sig. Den har tre tillstånd: Closed (normalt, anrop tillåts), Open (tjänsten är nere, anrop blockeras direkt) och Half-Open (testanrop skickas för att se om tjänsten är tillbaka).

Detta mönster skyddar både dig och den externa tjänsten. Utan det kan en kaskad av retries förvärra problemet och dra ner hela ditt system. Kombinera circuit breaker med fallback-logik: returnera cachad data eller ett degraderat svar medan tjänsten är nere.

lib/circuit-breaker.ts
typescript
// Circuit breaker implementation
type CircuitState = "closed" | "open" | "half-open";

class CircuitBreaker {
  private state: CircuitState = "closed";
  private failureCount = 0;
  private lastFailure = 0;
  private successCount = 0;

  constructor(
    private failureThreshold: number = 5,
    private resetTimeout: number = 30000,
    private halfOpenMax: number = 3
  ) {}

  async execute<T>(fn: () => Promise<T>, fallback?: () => T): Promise<T> {
    if (this.state === "open") {
      // Kolla om det är dags att testa igen
      if (Date.now() - this.lastFailure > this.resetTimeout) {
        this.state = "half-open";
        this.successCount = 0;
      } else if (fallback) {
        return fallback();
      } else {
        throw new Error("Circuit breaker är öppen");
      }
    }

    try {
      const result = await fn();

      if (this.state === "half-open") {
        this.successCount++;
        if (this.successCount >= this.halfOpenMax) {
          this.state = "closed";
          this.failureCount = 0;
        }
      }

      return result;
    } catch (err) {
      this.failureCount++;
      this.lastFailure = Date.now();

      if (this.failureCount >= this.failureThreshold) {
        this.state = "open";
        console.warn("Circuit breaker OPEN: tjänsten blockeras");
      }

      if (fallback && this.state === "open") {
        return fallback();
      }
      throw err;
    }
  }

  getState(): CircuitState {
    return this.state;
  }
}

// Användning
const paymentCircuit = new CircuitBreaker(5, 30000);

async function processPayment(orderId: string) {
  return paymentCircuit.execute(
    () => paymentApi.charge(orderId),
    () => ({ status: "queued", message: "Betalningen köas" })
  );
}

Logging och observerbarhet

Varje API-fel ska kunna spåras från klientens felmeddelande tillbaka till den exakta kodraden som orsakade problemet. Generera ett unikt request-ID för varje inkommande request och inkludera det i alla loggar och felmeddelanden.

Strukturerad logging (JSON-format) gör det möjligt att söka och filtrera i loggar effektivt. Logga alltid: request-ID, timestamp, HTTP-metod och URL, statuskod, responstid, klient-ID (API-nyckel eller user-ID) och felet i detalj.

Sätt upp alerts för ovanliga felmönster: plötslig ökning av 5xx-fel, specifika endpoints som börjar ge timeout, eller enstaka klienter som genererar oproportionerligt många fel. Dessa tidiga varningssignaler fångar problem innan de eskalerar.

Kombinera med monitoring av responstider. Ett API som svarar men tar 10 sekunder istället för 100 millisekunder är funktionellt trasigt även om det returnerar 200 OK. Läs mer om att skydda ditt API mot överbelastning i vår guide om rate limiting.