diff --git a/lib/datasource/cache.ts b/lib/datasource/cache.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8e687747a5e1b75d144588d050293ac226a075dc
--- /dev/null
+++ b/lib/datasource/cache.ts
@@ -0,0 +1,77 @@
+import { logger } from '../logger';
+
+/**
+ * Cache callback result which has to be returned by the `CacheCallback` function.
+ */
+export interface CacheResult<TResult = unknown> {
+  /**
+   * The data which should be added to the cache
+   */
+  data: TResult;
+  /**
+   * `data` can only be cached if this is not `true`
+   */
+  isPrivate?: boolean;
+}
+
+/**
+ * Simple helper type for defining the `CacheCallback` function return type
+ */
+export type CachePromise<TResult = unknown> = Promise<CacheResult<TResult>>;
+
+/**
+ * The callback function which is called on cache miss.
+ */
+export type CacheCallback<TArg, TResult = unknown> = (
+  lookup: TArg
+) => CachePromise<TResult>;
+
+export type CacheConfig<TArg, TResult> = {
+  /**
+   * Datasource id
+   */
+  id: string;
+  /**
+   * Cache key
+   */
+  lookup: TArg;
+  /**
+   * Callback to use on cache miss to load result
+   */
+  cb: CacheCallback<TArg, TResult>;
+  /**
+   * Time to cache result in minutes
+   */
+  minutes?: number;
+};
+
+/**
+ * Loads result from cache or from passed callback on cache miss.
+ * @param param0 Cache config args
+ */
+export async function cacheAble<TArg, TResult = unknown>({
+  id,
+  lookup,
+  cb,
+  minutes = 60,
+}: CacheConfig<TArg, TResult>): Promise<TResult> {
+  const cacheNamespace = `datasource-${id}`;
+  const cacheKey = JSON.stringify(lookup);
+  const cachedResult = await renovateCache.get<TResult>(
+    cacheNamespace,
+    cacheKey
+  );
+  // istanbul ignore if
+  if (cachedResult) {
+    logger.trace({ id, lookup }, 'datasource cachedResult');
+    return cachedResult;
+  }
+  const { data, isPrivate } = await cb(lookup);
+  // istanbul ignore if
+  if (isPrivate) {
+    logger.trace({ id, lookup }, 'Skipping datasource cache for private data');
+  } else {
+    await renovateCache.set(cacheNamespace, cacheKey, data, minutes);
+  }
+  return data;
+}
diff --git a/lib/datasource/cdnjs/index.spec.ts b/lib/datasource/cdnjs/index.spec.ts
index 2d1d43dc6f5f1cf44fea270c06168949c871b304..db16b940c8dcbcdc8697c05be4409e20c23cd8d0 100644
--- a/lib/datasource/cdnjs/index.spec.ts
+++ b/lib/datasource/cdnjs/index.spec.ts
@@ -30,17 +30,21 @@ describe('datasource/cdnjs', () => {
         DATASOURCE_FAILURE
       );
     });
-    it('returns null for missing fields', async () => {
+    it('throws for missing fields', async () => {
       got.mockResolvedValueOnce({});
-      expect(await getReleases({ lookupName: 'foo/bar' })).toBeNull();
+      await expect(getReleases({ lookupName: 'foo/bar' })).rejects.toThrowError(
+        DATASOURCE_FAILURE
+      );
     });
     it('returns null for 404', async () => {
       got.mockRejectedValueOnce({ statusCode: 404 });
       expect(await getReleases({ lookupName: 'foo/bar' })).toBeNull();
     });
-    it('returns null for 401', async () => {
+    it('throws for 401', async () => {
       got.mockRejectedValueOnce({ statusCode: 401 });
-      expect(await getReleases({ lookupName: 'foo/bar' })).toBeNull();
+      await expect(getReleases({ lookupName: 'foo/bar' })).rejects.toThrowError(
+        DATASOURCE_FAILURE
+      );
     });
     it('throws for 429', async () => {
       got.mockRejectedValueOnce({ statusCode: 429 });
@@ -62,11 +66,6 @@ describe('datasource/cdnjs', () => {
         DATASOURCE_FAILURE
       );
     });
-    it('returns null with wrong auth token', async () => {
-      got.mockRejectedValueOnce({ statusCode: 401 });
-      const res = await getReleases({ lookupName: 'foo/bar' });
-      expect(res).toBeNull();
-    });
     it('processes real data', async () => {
       got.mockResolvedValueOnce({ body: res1 });
       const res = await getReleases({ lookupName: 'd3-force/d3-force.js' });
diff --git a/lib/datasource/cdnjs/index.ts b/lib/datasource/cdnjs/index.ts
index 1bd8feb3d007b94942c4e60343a031e5318f4a4b..c74c1d2e025ee0eb7394a29cc2b22017fa3f8d45 100644
--- a/lib/datasource/cdnjs/index.ts
+++ b/lib/datasource/cdnjs/index.ts
@@ -1,6 +1,11 @@
 import { logger } from '../../logger';
 import { Http } from '../../util/http';
 import { DatasourceError, ReleaseResult, GetReleasesConfig } from '../common';
+import { cacheAble, CachePromise } from '../cache';
+
+export const id = 'cdnjs';
+
+const http = new Http(id);
 
 export interface CdnjsAsset {
   version: string;
@@ -8,13 +13,6 @@ export interface CdnjsAsset {
   sri?: Record<string, string>;
 }
 
-export const id = 'cdnjs';
-
-const http = new Http(id);
-
-const cacheNamespace = `datasource-${id}`;
-const cacheMinutes = 60;
-
 export interface CdnjsResponse {
   homepage?: string;
   repository?: {
@@ -24,40 +22,22 @@ export interface CdnjsResponse {
   assets?: CdnjsAsset[];
 }
 
-export function depUrl(library: string): string {
-  return `https://api.cdnjs.com/libraries/${library}?fields=homepage,repository,assets`;
+async function downloadLibrary(library: string): CachePromise<CdnjsResponse> {
+  const url = `https://api.cdnjs.com/libraries/${library}?fields=homepage,repository,assets`;
+  return { data: (await http.getJson<CdnjsResponse>(url)).body };
 }
 
 export async function getReleases({
   lookupName,
 }: GetReleasesConfig): Promise<ReleaseResult | null> {
-  const [library, ...assetParts] = lookupName.split('/');
-  const assetName = assetParts.join('/');
-
-  const cacheKey = library;
-  const cachedResult = await renovateCache.get<ReleaseResult>(
-    cacheNamespace,
-    cacheKey
-  );
-  // istanbul ignore if
-  if (cachedResult) {
-    return cachedResult;
-  }
-
-  const url = depUrl(library);
-
+  const library = lookupName.split('/')[0];
   try {
-    const res = await http.getJson(url);
-
-    const cdnjsResp: CdnjsResponse = res.body;
-
-    if (!cdnjsResp || !cdnjsResp.assets) {
-      logger.warn({ library }, `Invalid CDNJS response`);
-      return null;
-    }
-
-    const { assets, homepage, repository } = cdnjsResp;
-
+    const { assets, homepage, repository } = await cacheAble({
+      id,
+      lookup: library,
+      cb: downloadLibrary,
+    });
+    const assetName = lookupName.replace(`${library}/`, '');
     const releases = assets
       .filter(({ files }) => files.includes(assetName))
       .map(({ version, sri }) => ({ version, newDigest: sri[assetName] }));
@@ -67,31 +47,16 @@ export async function getReleases({
     if (homepage) {
       result.homepage = homepage;
     }
-    if (repository && repository.url) {
+    if (repository?.url) {
       result.sourceUrl = repository.url;
     }
-
-    await renovateCache.set(cacheNamespace, cacheKey, result, cacheMinutes);
-
     return result;
   } catch (err) {
-    const errorData = { library, err };
-
-    if (
-      err.statusCode === 429 ||
-      (err.statusCode >= 500 && err.statusCode < 600)
-    ) {
-      throw new DatasourceError(err);
-    }
-    if (err.statusCode === 401) {
-      logger.debug(errorData, 'Authorization error');
-    } else if (err.statusCode === 404) {
-      logger.debug(errorData, 'Package lookup error');
-    } else {
-      logger.debug(errorData, 'CDNJS lookup failure: Unknown error');
-      throw new DatasourceError(err);
+    if (err.statusCode === 404) {
+      logger.debug({ library, err }, 'Package lookup error');
+      return null;
     }
+    // Throw a DatasourceError for all other types of errors
+    throw new DatasourceError(err);
   }
-
-  return null;
 }