Skip to main content

Axios vs Fetch vs Got: HTTP Clients 2026

·PkgPulse Team
0

TL;DR

Axios for universal apps that need interceptors and browser support. Fetch (native) for serverless functions and simple use cases where zero-dependency matters. Got for Node.js-only services that need advanced features like automatic retries, pagination, and streaming. Axios leads with ~55M weekly downloads but ships 40KB+ to browsers. Node.js 18+ ships native fetch that's now production-ready. Got is the power tool for complex server-side HTTP scenarios.

Quick Comparison

Axios v1.xFetch (native)Got v14
Weekly Downloads~55MBuilt-in~9M
Bundle Size~40KB gzipped0KB~85KB (Node-only)
Browser SupportYesYes (modern)No (Node.js only)
Request InterceptorsYesNoVia hooks
Automatic RetriesNo (plugin)NoYes (built-in)
Response TypeJSON auto-parsedRequires .json()JSON auto-parsed
Error on 4xx/5xxYesNo (manual check)Yes
TypeScriptGoodGoodExcellent
StreamingBasicGoodExcellent
PaginationNoNoBuilt-in

Error Handling: The Biggest Practical Difference

The single most consequential difference between Axios and native fetch is how they handle HTTP errors. Fetch considers any completed HTTP request a success — including 404, 500, and 503. You must manually check response.ok or response.status to detect errors.

// Fetch — silent 404/500 errors (common gotcha)
const response = await fetch('/api/users/999');
// response.ok is false for 404, but no exception thrown!
const data = await response.json(); // Parses the error body

// You MUST check:
if (!response.ok) {
  throw new Error(`HTTP error: ${response.status}`);
}

// Every fetch call needs this boilerplate, or you'll silently swallow errors

Axios throws an error automatically for any status code outside 2xx. The error object contains the full response, including the error body:

// Axios — automatic error throwing
try {
  const { data } = await axios.get('/api/users/999');
  // Only runs if status is 2xx
} catch (error) {
  if (error.response) {
    // Server responded with a non-2xx status
    console.log(error.response.status);   // 404
    console.log(error.response.data);     // Error body from server
    console.log(error.response.headers);  // Response headers
  } else if (error.request) {
    // Request was made but no response received (network error)
    console.log('Network error');
  }
}

Got has the same automatic-throw behavior as Axios. Got's error classes are more granular — RequestError, ReadError, ParseError, HTTPError — and include full request/response context. For production services where error categorization matters, Got's typed errors make error handling more precise.

import { got, HTTPError, RequestError } from 'got';

try {
  const data = await got('https://api.example.com/users/999').json();
} catch (error) {
  if (error instanceof HTTPError) {
    // HTTP 4xx/5xx error
    console.log(error.response.statusCode);
    console.log(error.response.body);     // Already parsed if JSON
  } else if (error instanceof RequestError) {
    // Network-level error (DNS, timeout, connection refused)
    console.log(error.code); // 'ECONNREFUSED', 'ETIMEDOUT', etc.
  }
}

Interceptors: Axios's Core Strength

Axios's interceptor system is the feature that keeps teams on Axios long after other alternatives emerge. Interceptors let you attach middleware to every request and response without modifying individual callsites.

// Axios interceptors — authentication + logging + error normalization
const client = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
});

// Request interceptor: attach auth token
client.interceptors.request.use((config) => {
  const token = getAuthToken();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor: normalize errors + auto-refresh tokens
client.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401 && !error.config._retry) {
      error.config._retry = true;
      const newToken = await refreshAuthToken();
      error.config.headers.Authorization = `Bearer ${newToken}`;
      return client(error.config); // Retry with new token
    }
    return Promise.reject(error);
  }
);

This pattern — centralizing auth, logging, and error handling in interceptors — is something fetch requires significant boilerplate to replicate. Got's hooks system provides equivalent capability but with a different (arguably more composable) API:

import { got } from 'got';

const client = got.extend({
  prefixUrl: 'https://api.example.com',
  timeout: { request: 10000 },
  hooks: {
    beforeRequest: [
      (options) => {
        options.headers['Authorization'] = `Bearer ${getAuthToken()}`;
      }
    ],
    afterResponse: [
      async (response, retryWithMergedOptions) => {
        if (response.statusCode === 401) {
          const newToken = await refreshAuthToken();
          return retryWithMergedOptions({
            headers: { Authorization: `Bearer ${newToken}` }
          });
        }
        return response;
      }
    ],
    beforeError: [
      (error) => {
        // Normalize errors before they reach your code
        error.customCode = mapStatusToCode(error.response?.statusCode);
        return error;
      }
    ]
  }
});

Retries and Resilience

Neither Axios nor native fetch includes built-in retry logic. Got's retry capability is built in and configurable per-instance:

// Got — built-in retry with exponential backoff
import { got } from 'got';

const resilientClient = got.extend({
  retry: {
    limit: 3,
    statusCodes: [408, 429, 500, 502, 503, 504],
    errorCodes: ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'],
    methods: ['GET', 'PUT', 'HEAD', 'DELETE', 'OPTIONS', 'TRACE'],
    calculateDelay: ({ retryCount }) => retryCount * 1000, // Linear backoff
  },
  hooks: {
    beforeRetry: [
      ({ retryCount }) => console.log(`Retry attempt #${retryCount}`)
    ]
  }
});

// Automatically retries on 503, network errors, etc.
const data = await resilientClient('https://api.example.com/endpoint').json();

For Axios, the community maintains axios-retry:

import axios from 'axios';
import axiosRetry from 'axios-retry';

axiosRetry(axios, {
  retries: 3,
  retryDelay: axiosRetry.exponentialDelay,
  retryCondition: (error) => {
    return axiosRetry.isNetworkOrIdempotentRequestError(error)
      || error.response?.status === 429;
  }
});

For fetch, you implement retry yourself or use a wrapper library. This is manageable but adds maintenance surface.


Streaming Large Responses

For downloading files, streaming large API responses, or processing Server-Sent Events, the three approaches differ significantly.

Got's streaming is first-class — you can pipe Got's stream directly to any Node.js writable:

import { got } from 'got';
import { createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';

// Stream a large file download
await pipeline(
  got.stream('https://example.com/large-file.csv'),
  createWriteStream('./output.csv')
);

// Process a large JSON array as a stream
import { parse } from 'JSONStream';
const stream = got.stream('https://api.example.com/export');
stream
  .pipe(parse('*'))
  .on('data', (item) => processItem(item));

Fetch has native streaming via ReadableStream and is well-suited for browser streaming (SSE, streaming AI responses):

// Fetch — streaming AI/SSE response
const response = await fetch('/api/stream');
const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  const chunk = decoder.decode(value, { stream: true });
  process.stdout.write(chunk);
}

Axios's streaming support is functional but less ergonomic than Got's — it works through the responseType: 'stream' option on the Node.js side.


Bundle Size and Universal Compatibility

Axios's ~40KB gzipped footprint is the main argument against it in frontend code. For a React app where every KB counts, importing Axios adds 40KB to your bundle. Native fetch adds 0KB — it's already in the browser.

// For browser-heavy apps, native fetch is often enough:
const fetchWithAuth = async (url, options = {}) => {
  const response = await fetch(url, {
    ...options,
    headers: {
      'Authorization': `Bearer ${getToken()}`,
      'Content-Type': 'application/json',
      ...options.headers,
    },
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
};

// This handles 80% of frontend HTTP needs with zero bundle cost

Where Axios earns its 40KB is in the universal use case — the same API instance runs in both the browser and Node.js, with identical interceptors. For projects using Axios on both client and server (Next.js server actions sharing a client instance, for example), this universality avoids duplicating HTTP client logic.

Got is Node.js only, making it inappropriate for browser bundles or universal code. It's specifically for server-side and CLI tooling.


Pagination Support

Got's pagination API is unique among HTTP clients — it handles cursor-based, offset-based, and Link-header-based pagination natively:

import { got } from 'got';

// Automatically follow pagination — get all results
const allUsers = await got.paginate.all('https://api.github.com/users', {
  pagination: {
    transform: (response) => JSON.parse(response.body),
    paginate: ({ response }) => {
      const linkHeader = response.headers.link;
      const nextUrl = parseLinkHeader(linkHeader)?.next;
      return nextUrl ? { url: nextUrl } : false;
    },
  },
});

// Or process page-by-page
for await (const user of got.paginate('https://api.github.com/users')) {
  console.log(user.login);
}

This built-in pagination is compelling for data pipeline code that must iterate over large API datasets. With Axios or fetch, you write this loop yourself every time.


When to Use Which

Choose Axios when:

  • You need the same HTTP client in both browser and Node.js
  • Request/response interceptors are central to your architecture (auth, logging, error normalization)
  • Your team already uses Axios and migration cost isn't justified

Choose native Fetch when:

  • You're writing browser-only code or serverless functions
  • Zero-dependency footprint matters
  • You're building Next.js App Router server components or server actions (fetch is built-in)
  • Your use case is simple enough that manual response.ok checks are acceptable

Choose Got when:

  • Your code is Node.js-only (backend services, CLIs, scripts)
  • You need built-in retries, hooks, or pagination
  • You're downloading large files or streaming data
  • You want the most complete HTTP client feature set without building your own

The HTTP client space has split along runtime lines: Axios remains dominant for universal JavaScript code, native fetch has become viable for serverless and simple frontend use, and Got is the specialist tool for complex Node.js HTTP scenarios. For teams building purely server-side Node.js services in 2026, Got's built-in resilience features (retries, timeouts, pagination) deliver meaningful productivity compared to building equivalent logic on top of Axios or fetch.

Compare Axios and related packages on PkgPulse. See also axios alternatives 2026: got, ky, and undici and got vs undici vs node-fetch HTTP clients.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.