diff --git a/src/auth/providers/users-permissions.ts b/src/auth/providers/users-permissions.ts
index 3eb8b90..d1c27c0 100644
--- a/src/auth/providers/users-permissions.ts
+++ b/src/auth/providers/users-permissions.ts
@@ -1,4 +1,4 @@
-import { StrapiSDKError, StrapiSDKValidationError } from '../../errors';
+import { StrapiSDKValidationError } from '../../errors';
 import { HttpClient } from '../../http';
 
 import { AbstractAuthProvider } from './abstract';
@@ -28,11 +28,11 @@ export type UsersPermissionsAuthPayload = Pick<
   'identifier' | 'password'
 >;
 
-  /**
-   * @experimental
-   * Authentication through users and permissions is experimental for the MVP of
-   * the Strapi SDK.
-   */
+/**
+ * @experimental
+ * Authentication through users and permissions is experimental for the MVP of
+ * the Strapi SDK.
+ */
 export class UsersPermissionsAuthProvider extends AbstractAuthProvider<UsersPermissionsAuthProviderOptions> {
   public static readonly identifier = USERS_PERMISSIONS_AUTH_STRATEGY_IDENTIFIER;
 
@@ -88,29 +88,19 @@ export class UsersPermissionsAuthProvider extends AbstractAuthProvider<UsersPerm
   }
 
   async authenticate(httpClient: HttpClient): Promise<void> {
-    try {
-      const { baseURL } = httpClient;
-      const localAuthURL = `${baseURL}/auth/local`;
-
-      const request = new Request(localAuthURL, {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify(this.credentials),
-      });
-
-      // Make sure to use the HttpClient's "_fetch" method to not perform authentication in an infinite loop.
-      const response = await httpClient._fetch(request);
-
-      if (!response.ok) {
-        // TODO: use dedicated exceptions
-        throw new Error(response.statusText);
-      }
-
-      const data = await response.json();
-      this._token = data.jwt;
-    } catch (error) {
-      // TODO: use dedicated exceptions
-      throw new StrapiSDKError(error, 'Failed to authenticate with Strapi server.');
-    }
+    const { baseURL } = httpClient;
+    const localAuthURL = `${baseURL}/auth/local`;
+
+    const request = new Request(localAuthURL, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(this.credentials),
+    });
+
+    // Make sure to use the HttpClient's "_fetch" method to not perform authentication in an infinite loop.
+    const response = await httpClient._fetch(request);
+    const data = await response.json();
+
+    this._token = data.jwt;
   }
 }
diff --git a/src/errors/http.ts b/src/errors/http.ts
new file mode 100644
index 0000000..0d2e859
--- /dev/null
+++ b/src/errors/http.ts
@@ -0,0 +1,41 @@
+export class HTTPError extends Error {
+  public name = 'HTTPError';
+  public response: Response;
+  public request: Request;
+
+  constructor(response: Response, request: Request) {
+    const code: string = response.status?.toString() ?? '';
+    const title = response.statusText ?? '';
+    const status = `${code} ${title}`.trim();
+    const reason = status ? `status code ${status}` : 'an unknown error';
+
+    super(`Request failed with ${reason}: ${request.method} ${request.url}`);
+
+    this.response = response;
+    this.request = request;
+  }
+}
+
+export class HTTPAuthorizationError extends HTTPError {
+  public name = 'HTTPAuthorizationError';
+}
+
+export class HTTPNotFoundError extends HTTPError {
+  public name = 'HTTPNotFoundError';
+}
+
+export class HTTPBadRequestError extends HTTPError {
+  public name = 'HTTPBadRequestError';
+}
+
+export class HTTPInternalServerError extends HTTPError {
+  public name = 'HTTPInternalServerError';
+}
+
+export class HTTPForbiddenError extends HTTPError {
+  public name = 'HTTPForbiddenError';
+}
+
+export class HTTPTimeoutError extends HTTPError {
+  public name = 'HTTPTimeoutError';
+}
diff --git a/src/errors/index.ts b/src/errors/index.ts
index ecd846c..01db27f 100644
--- a/src/errors/index.ts
+++ b/src/errors/index.ts
@@ -1,2 +1,3 @@
 export * from './sdk';
 export * from './url';
+export * from './http';
diff --git a/src/http/client.ts b/src/http/client.ts
index 515da8b..970e9f3 100644
--- a/src/http/client.ts
+++ b/src/http/client.ts
@@ -1,6 +1,17 @@
 import { AuthManager } from '../auth';
+import {
+  HTTPAuthorizationError,
+  HTTPBadRequestError,
+  HTTPError,
+  HTTPForbiddenError,
+  HTTPInternalServerError,
+  HTTPNotFoundError,
+  HTTPTimeoutError,
+} from '../errors';
 import { URLValidator } from '../validators';
 
+import { StatusCode } from './constants';
+
 export type Fetch = typeof globalThis.fetch;
 
 /**
@@ -52,7 +63,6 @@ export class HttpClient {
    * @returns The HttpClient instance for chaining.
    *
    * @throws {URLParsingError} If the URL cannot be parsed.
-   * @throws {URLProtocolValidationError} If the URL uses an unsupported protocol.
    *
    * @example
    * const client = new HttpClient('http://example.com');
@@ -114,30 +124,54 @@ export class HttpClient {
 
     this.attachHeaders(request);
 
-    const response = await this._fetch(request);
+    try {
+      return await this._fetch(request);
+    } catch (e) {
+      this.handleFetchError(e);
 
-    if (response.status === 401) {
-      this._authManager.handleUnauthorizedError();
+      throw e;
     }
+  }
 
-    return response;
+  /**
+   * Handles HTTP fetch error logic.
+   *
+   * It deals with unauthorized responses by delegating the handling of the error to the authentication manager.
+   *
+   * @param error - The original HTTP request object that encountered an error. Used for error handling.
+   *
+   * @see {@link AuthManager#handleUnauthorizedError} for handling unauthorized responses.
+   */
+  private handleFetchError(error: unknown) {
+    if (error instanceof HTTPAuthorizationError) {
+      this._authManager.handleUnauthorizedError();
+    }
   }
 
   /**
    * Executes an HTTP fetch request using the Fetch API.
    *
-   * @param url - The target URL for the HTTP request which can be a string URL or a `Request` object.
+   * @param input - The target of the HTTP request which can be a string URL or a `Request` object.
    * @param [init] - An optional `RequestInit` object that contains any custom settings that you want to apply to the request.
    *
    * @returns A promise that resolves to the `Response` object representing the complete HTTP response.
    *
+   * @throws {HTTPError} if the request fails
+   *
    * @additionalInfo
    * - This method doesn't perform any authentication or header customization.
    *   It directly passes the parameters to the global `fetch` function.
    * - To include authentication, consider using the `fetch` method from the `HttpClient` class, which handles headers and authentication.
    */
-  async _fetch(url: RequestInfo, init?: RequestInit): Promise<Response> {
-    return globalThis.fetch(url, init);
+  async _fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {
+    const request = new Request(input, init);
+    const response = await globalThis.fetch(request);
+
+    if (!response.ok) {
+      throw this.mapResponseToHTTPError(response, request);
+    }
+
+    return response;
   }
 
   /**
@@ -159,4 +193,36 @@ export class HttpClient {
     // Set auth headers if available, potentially overwrite manually set auth headers
     this._authManager.authenticateRequest(request);
   }
+
+  /**
+   * Maps an HTTP response's status code to a specific HTTP error class.
+   *
+   * @param response - The HTTP response object obtained from a failed HTTP request,
+   *                   which contains the status code and reason for failure.
+   * @param request - The original HTTP request object that resulted in the error response.
+   *
+   * @returns A specific subclass instance of HTTPError based on the response status code.
+   *
+   * @throws {HTTPError} or any of its subclass.
+   *
+   * @see {@link StatusCode} for all possible HTTP status codes and their meanings.
+   */
+  private mapResponseToHTTPError(response: Response, request: Request): HTTPError {
+    switch (response.status) {
+      case StatusCode.BAD_REQUEST:
+        return new HTTPBadRequestError(response, request);
+      case StatusCode.UNAUTHORIZED:
+        return new HTTPAuthorizationError(response, request);
+      case StatusCode.FORBIDDEN:
+        return new HTTPForbiddenError(response, request);
+      case StatusCode.NOT_FOUND:
+        return new HTTPNotFoundError(response, request);
+      case StatusCode.TIMEOUT:
+        return new HTTPTimeoutError(response, request);
+      case StatusCode.INTERNAL_SERVER_ERROR:
+        return new HTTPInternalServerError(response, request);
+    }
+
+    return new HTTPError(response, request);
+  }
 }
diff --git a/src/http/constants.ts b/src/http/constants.ts
new file mode 100644
index 0000000..79b77bd
--- /dev/null
+++ b/src/http/constants.ts
@@ -0,0 +1,11 @@
+export enum StatusCode {
+  OK = 200,
+  CREATED = 201,
+  NO_CONTENT = 204,
+  BAD_REQUEST = 400,
+  UNAUTHORIZED = 401,
+  FORBIDDEN = 403,
+  NOT_FOUND = 404,
+  TIMEOUT = 408,
+  INTERNAL_SERVER_ERROR = 500,
+}
diff --git a/src/http/index.ts b/src/http/index.ts
index 4f1cce4..217d867 100644
--- a/src/http/index.ts
+++ b/src/http/index.ts
@@ -1 +1,2 @@
 export * from './client';
+export * from './constants';
diff --git a/tests/unit/auth/providers/users-permissions.test.ts b/tests/unit/auth/providers/users-permissions.test.ts
index 71fe807..4eeb49a 100644
--- a/tests/unit/auth/providers/users-permissions.test.ts
+++ b/tests/unit/auth/providers/users-permissions.test.ts
@@ -2,9 +2,9 @@ import {
   UsersPermissionsAuthProvider,
   UsersPermissionsAuthProviderOptions,
 } from '../../../../src/auth';
-import { StrapiSDKError, StrapiSDKValidationError } from '../../../../src/errors';
+import { HTTPBadRequestError, StrapiSDKValidationError } from '../../../../src/errors';
 import { HttpClient } from '../../../../src/http';
-import { MockHttpClient } from '../../mocks';
+import { MockHttpClient, mockRequest, mockResponse } from '../../mocks';
 
 const FAKE_TOKEN = '<token>';
 const FAKE_VALID_CONFIG: UsersPermissionsAuthProviderOptions = {
@@ -19,8 +19,11 @@ class ValidFakeHttpClient extends MockHttpClient {
 }
 
 class FaultyFakeHttpClient extends HttpClient {
-  async _fetch() {
-    return new Response('Bad request', { status: 400 });
+  async _fetch(): Promise<Response> {
+    const response = mockResponse(400, 'Bad Request');
+    const request = mockRequest('GET', 'https://example.com');
+
+    throw new HTTPBadRequestError(response, request);
   }
 }
 
@@ -111,7 +114,7 @@ describe('UsersPermissionsAuthProvider', () => {
       const provider = new UsersPermissionsAuthProvider(FAKE_VALID_CONFIG);
 
       // Act & Assert
-      await expect(provider.authenticate(faultyHttpClient)).rejects.toThrow(StrapiSDKError);
+      await expect(provider.authenticate(faultyHttpClient)).rejects.toThrow(HTTPBadRequestError);
     });
   });
 
diff --git a/tests/unit/errors/http-errors.test.ts b/tests/unit/errors/http-errors.test.ts
new file mode 100644
index 0000000..8a2c71d
--- /dev/null
+++ b/tests/unit/errors/http-errors.test.ts
@@ -0,0 +1,85 @@
+import {
+  HTTPAuthorizationError,
+  HTTPBadRequestError,
+  HTTPError,
+  HTTPForbiddenError,
+  HTTPInternalServerError,
+  HTTPNotFoundError,
+  HTTPTimeoutError,
+} from '../../../src/errors';
+import { StatusCode } from '../../../src/http';
+import { mockRequest, mockResponse } from '../mocks';
+
+describe('HTTP Errors', () => {
+  describe('HTTPError', () => {
+    it('should correctly instantiate with a status code and status text', () => {
+      // Arrange
+      const response = mockResponse(504, 'Gateway Timeout');
+      const request = mockRequest('GET', 'https://example.com/resource');
+
+      // Act
+      const error = new HTTPError(response, request);
+
+      // Assert
+      expect(error.name).toBe('HTTPError');
+      expect(error.message).toBe(
+        'Request failed with status code 504 Gateway Timeout: GET https://example.com/resource'
+      );
+      expect(error.response).toBe(response);
+      expect(error.request).toBe(request);
+    });
+
+    it('should handle status code without status text', () => {
+      // Arrange
+      const response = mockResponse(500, '');
+      const request = mockRequest('POST', 'https://example.com/update');
+
+      // Act
+      const error = new HTTPError(response, request);
+
+      // Assert
+      expect(error.message).toBe(
+        'Request failed with status code 500: POST https://example.com/update'
+      );
+    });
+
+    it('should handle requests with no status code', () => {
+      // Arrange
+      const response = mockResponse(undefined as any, '');
+      const request = mockRequest('GET', 'https://example.com/unknown');
+
+      // Act
+      const error = new HTTPError(response, request);
+
+      // Assert
+      expect(error.message).toBe(
+        'Request failed with an unknown error: GET https://example.com/unknown'
+      );
+    });
+  });
+
+  it.each([
+    [HTTPBadRequestError.name, HTTPBadRequestError, StatusCode.BAD_REQUEST],
+    [HTTPAuthorizationError.name, HTTPAuthorizationError, StatusCode.UNAUTHORIZED],
+    [HTTPForbiddenError.name, HTTPForbiddenError, StatusCode.FORBIDDEN],
+    [HTTPNotFoundError.name, HTTPNotFoundError, StatusCode.NOT_FOUND],
+    [HTTPTimeoutError.name, HTTPTimeoutError, StatusCode.TIMEOUT],
+    [HTTPInternalServerError.name, HTTPInternalServerError, StatusCode.INTERNAL_SERVER_ERROR],
+  ])('%s', (name, errorClass, status) => {
+    // Arrange
+    const response = mockResponse(status, name);
+    const request = mockRequest('GET', 'https://example.com');
+
+    // Act
+    const error = new errorClass(response, request);
+
+    // Assert
+    expect(error).toBeInstanceOf(HTTPError);
+    expect(error.name).toBe(name);
+    expect(error.message).toBe(
+      `Request failed with status code ${status} ${name}: GET https://example.com`
+    );
+    expect(error.response).toBe(response);
+    expect(error.request).toBe(request);
+  });
+});
diff --git a/tests/unit/errors/sdk.test.ts b/tests/unit/errors/sdk-errors.test.ts
similarity index 100%
rename from tests/unit/errors/sdk.test.ts
rename to tests/unit/errors/sdk-errors.test.ts
diff --git a/tests/unit/errors/url.test.ts b/tests/unit/errors/url-errors.test.ts
similarity index 100%
rename from tests/unit/errors/url.test.ts
rename to tests/unit/errors/url-errors.test.ts
diff --git a/tests/unit/http/client.test.ts b/tests/unit/http/client.test.ts
index ecf7293..833e73b 100644
--- a/tests/unit/http/client.test.ts
+++ b/tests/unit/http/client.test.ts
@@ -1,4 +1,13 @@
-import { HttpClient } from '../../../src/http';
+import {
+  HTTPAuthorizationError,
+  HTTPBadRequestError,
+  HTTPError,
+  HTTPForbiddenError,
+  HTTPInternalServerError,
+  HTTPNotFoundError,
+  HTTPTimeoutError,
+} from '../../../src/errors';
+import { HttpClient, StatusCode } from '../../../src/http';
 import { MockAuthManager, MockAuthProvider, MockURLValidator } from '../mocks';
 
 describe('HttpClient', () => {
@@ -145,8 +154,6 @@ describe('HttpClient', () => {
 
     // Assert
     expect(mockAuthManager.isAuthenticated).toBe(true);
-    expect(fetchSpy).toHaveBeenCalledWith(expect.any(Request), undefined);
-    expect(response.status).toBe(200);
     await expect(response.json()).resolves.toEqual(payload);
   });
 
@@ -161,13 +168,29 @@ describe('HttpClient', () => {
     const httpClient = new HttpClient('https://example.com', mockAuthManager, mockURLValidator);
     httpClient.setAuthStrategy(MockAuthProvider.identifier, {});
 
-    // Act
-    const response = await httpClient.fetch('/');
+    // Act & Assert
+    await expect(httpClient.fetch('/')).rejects.toThrow(HTTPAuthorizationError);
 
-    // Assert
     expect(handleUnauthorizedErrorSpy).toHaveBeenCalled();
     expect(mockAuthManager.isAuthenticated).toBe(false);
-    expect(fetchSpy).toHaveBeenCalledWith(expect.any(Request), undefined);
-    expect(response.status).toBe(401);
+  });
+
+  describe('Error Mapping', () => {
+    it.each([
+      ['Bad Request', StatusCode.BAD_REQUEST, HTTPBadRequestError],
+      ['Unauthorized', StatusCode.UNAUTHORIZED, HTTPAuthorizationError],
+      ['Forbidden', StatusCode.FORBIDDEN, HTTPForbiddenError],
+      ['Not Found', StatusCode.NOT_FOUND, HTTPNotFoundError],
+      ['Timeout', StatusCode.TIMEOUT, HTTPTimeoutError],
+      ['Internal Server', StatusCode.INTERNAL_SERVER_ERROR, HTTPInternalServerError],
+      ['Unknown', 504, HTTPError],
+    ])('should throw on %s error', async (_name, status, error) => {
+      // Arrange
+      fetchSpy.mockImplementationOnce(() => Promise.resolve(new Response('', { status })));
+      const httpClient = new HttpClient('https://example.com', mockAuthManager, mockURLValidator);
+
+      // Act & Assert
+      await expect(httpClient.fetch('/foo')).rejects.toThrow(error);
+    });
   });
 });
diff --git a/tests/unit/mocks/index.ts b/tests/unit/mocks/index.ts
index 1d525ff..6242bf0 100644
--- a/tests/unit/mocks/index.ts
+++ b/tests/unit/mocks/index.ts
@@ -4,3 +4,5 @@ export { MockAuthManager } from './auth-manager.mock';
 export { MockHttpClient } from './http-client.mock';
 export { MockURLValidator } from './url-validator.mock';
 export { MockStrapiSDKValidator } from './sdk-validator.mock';
+export { mockRequest } from './request.mock';
+export { mockResponse } from './response.mock';
diff --git a/tests/unit/mocks/request.mock.ts b/tests/unit/mocks/request.mock.ts
new file mode 100644
index 0000000..f13e4aa
--- /dev/null
+++ b/tests/unit/mocks/request.mock.ts
@@ -0,0 +1,23 @@
+export const mockRequest = (method: string, url: string): Request => ({
+  method,
+  url,
+  headers: new Headers(),
+  redirect: 'follow',
+  clone: jest.fn(),
+  body: null,
+  bodyUsed: false,
+  cache: 'default',
+  credentials: 'same-origin',
+  integrity: '',
+  keepalive: false,
+  mode: 'same-origin',
+  referrer: '',
+  referrerPolicy: 'no-referrer',
+  destination: '',
+  signal: AbortSignal.any([]),
+  arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(0)),
+  blob: jest.fn().mockResolvedValue(new Blob()),
+  formData: jest.fn().mockResolvedValue(new FormData()),
+  text: jest.fn().mockResolvedValue(''),
+  json: jest.fn().mockResolvedValue({}),
+});
diff --git a/tests/unit/mocks/response.mock.ts b/tests/unit/mocks/response.mock.ts
new file mode 100644
index 0000000..0edf191
--- /dev/null
+++ b/tests/unit/mocks/response.mock.ts
@@ -0,0 +1,17 @@
+export const mockResponse = (status: number, statusText: string): Response => ({
+  status,
+  statusText,
+  headers: new Headers(),
+  ok: status >= 200 && status < 300,
+  redirected: false,
+  type: 'basic',
+  url: 'https://example.com',
+  clone: jest.fn(),
+  body: null,
+  bodyUsed: false,
+  text: jest.fn().mockResolvedValue(''),
+  json: jest.fn().mockResolvedValue({}),
+  blob: jest.fn().mockResolvedValue(new Blob()),
+  formData: jest.fn().mockResolvedValue(new FormData()),
+  arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(0)),
+});