diff --git a/src/core/evaluator.js b/src/core/evaluator.js
index 56f947c91..c5e589824 100644
--- a/src/core/evaluator.js
+++ b/src/core/evaluator.js
@@ -19,8 +19,9 @@
            info, isArray, isCmd, isDict, isEOF, isName, isNum,
            isStream, isString, JpegStream, Lexer, Metrics, Name, Parser,
            Pattern, PDFImage, PDFJS, serifFonts, stdFontMap, symbolsFonts,
-           TilingPattern, TODO, warn, Util, Promise,
-           RefSetCache, isRef, TextRenderingMode, CMapFactory, OPS */
+           TilingPattern, warn, Util, Promise, UnsupportedManager,
+           RefSetCache, isRef, TextRenderingMode, CMapFactory, OPS,
+           UNSUPPORTED_FEATURES */
 
 'use strict';
 
@@ -407,9 +408,9 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
             gStateObj.push([key, value]);
             break;
           case 'SMask':
-            // We support the default so don't trigger the TODO.
+            // We support the default so don't trigger a warning bar.
             if (!isName(value) || value.name != 'None')
-              TODO('graphic state operator ' + key);
+              UnsupportedManager.notify(UNSUPPORTED_FEATURES.smask);
             break;
           // Only generate info log messages for the following since
           // they are unlikey to have a big impact on the rendering.
@@ -499,6 +500,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
         try {
           translated = this.translateFont(font, xref);
         } catch (e) {
+          UnsupportedManager.notify(UNSUPPORTED_FEATURES.font);
           translated = new ErrorFont(e instanceof Error ? e.message : e);
         }
         font.translated = translated;
diff --git a/src/core/fonts.js b/src/core/fonts.js
index dae633038..67236c3fe 100644
--- a/src/core/fonts.js
+++ b/src/core/fonts.js
@@ -17,7 +17,7 @@
 /* globals assert, bytesToString, CIDToUnicodeMaps, error, ExpertCharset,
            ExpertSubsetCharset, FileReaderSync, GlyphsUnicode,
            info, isArray, isNum, ISOAdobeCharset, Stream,
-           stringToBytes, TextDecoder, TODO, warn, Lexer, Util,
+           stringToBytes, TextDecoder, warn, Lexer, Util,
            FONT_IDENTITY_MATRIX, FontRendererFactory, shadow, isString */
 
 'use strict';
@@ -495,7 +495,7 @@ function sjis83pvToUnicode(str) {
     // TODO: 83pv has incompatible mappings in ed40..ee9c range.
     return decodeBytes(bytes, 'shift_jis', true);
   } catch (e) {
-    TODO('Unsupported 83pv character found');
+    warn('Unsupported 83pv character found');
     // Just retry without checking errors for now.
     return decodeBytes(bytes, 'shift_jis');
   }
@@ -507,7 +507,7 @@ function sjis90pvToUnicode(str) {
     // TODO: 90pv has incompatible mappings in 8740..879c and eb41..ee9c.
     return decodeBytes(bytes, 'shift_jis', true);
   } catch (e) {
-    TODO('Unsupported 90pv character found');
+    warn('Unsupported 90pv character found');
     // Just retry without checking errors for now.
     return decodeBytes(bytes, 'shift_jis');
   }
@@ -4339,7 +4339,7 @@ var Font = (function FontClosure() {
       var cidEncoding = properties.cidEncoding;
       if (properties.toUnicode) {
         if (cidEncoding && cidEncoding.indexOf('Identity-') !== 0) {
-          TODO('Need to create a reverse mapping from \'ToUnicode\' CMap');
+          warn('Need to create a reverse mapping from \'ToUnicode\' CMap');
         }
         return; // 'ToUnicode' CMap will be used
       }
diff --git a/src/core/image.js b/src/core/image.js
index ceeee3923..ca50f8dde 100644
--- a/src/core/image.js
+++ b/src/core/image.js
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 /* globals ColorSpace, error, isArray, isStream, JpegStream, Name, Promise,
-           Stream, TODO, warn */
+           Stream, warn */
 
 'use strict';
 
@@ -54,7 +54,7 @@ var PDFImage = (function PDFImageClosure() {
     if (image.getParams) {
       // JPX/JPEG2000 streams directly contain bits per component
       // and color space mode information.
-      TODO('get params from actual stream');
+      warn('get params from actual stream');
       // var bits = ...
       // var colorspace = ...
     }
@@ -87,7 +87,7 @@ var PDFImage = (function PDFImageClosure() {
     if (!this.imageMask) {
       var colorSpace = dict.get('ColorSpace', 'CS');
       if (!colorSpace) {
-        TODO('JPX images (which don"t require color spaces');
+        warn('JPX images (which don"t require color spaces');
         colorSpace = new Name('DeviceRGB');
       }
       this.colorSpace = ColorSpace.parse(colorSpace, xref, res);
diff --git a/src/core/worker.js b/src/core/worker.js
index 0db5b5a9b..a2b405b69 100644
--- a/src/core/worker.js
+++ b/src/core/worker.js
@@ -429,15 +429,12 @@ var workerConsole = {
 if (typeof window === 'undefined') {
   globalScope.console = workerConsole;
 
-  // Add a logger so we can pass warnings on to the main thread, errors will
-  // throw an exception which will be forwarded on automatically.
-  PDFJS.LogManager.addLogger({
-    warn: function(msg) {
-      globalScope.postMessage({
-        action: '_warn',
-        data: msg
-      });
-    }
+  // Listen for unsupported features so we can pass them on to the main thread.
+  PDFJS.UnsupportedManager.listen(function (msg) {
+    globalScope.postMessage({
+      action: '_unsupported_feature',
+      data: msg
+    });
   });
 
   var handler = new MessageHandler('worker_processor', this);
diff --git a/src/display/canvas.js b/src/display/canvas.js
index 026849298..a918b7bb5 100644
--- a/src/display/canvas.js
+++ b/src/display/canvas.js
@@ -16,7 +16,7 @@
  */
 /* globals ColorSpace, DeviceCmykCS, DeviceGrayCS, DeviceRgbCS, error,
            FONT_IDENTITY_MATRIX, IDENTITY_MATRIX, ImageData, isArray, isNum,
-           Pattern, TilingPattern, TODO, Util, warn, assert, info,
+           Pattern, TilingPattern, Util, warn, assert, info,
            TextRenderingMode, OPS */
 
 'use strict';
@@ -1460,7 +1460,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
       // TODO knockout - supposedly possible with the clever use of compositing
       // modes.
       if (group.knockout) {
-        TODO('Support knockout groups.');
+        warn('Knockout groups not supported.');
       }
 
       var currentTransform = currentCtx.mozCurrentTransform;
diff --git a/src/shared/annotation.js b/src/shared/annotation.js
index 67365f22f..eae51c980 100644
--- a/src/shared/annotation.js
+++ b/src/shared/annotation.js
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-/* globals Util, isDict, isName, stringToPDFString, TODO, Dict, Stream,
+/* globals Util, isDict, isName, stringToPDFString, warn, Dict, Stream,
            stringToBytes, PDFJS, isWorker, assert, NotImplementedException,
            Promise, isArray, ObjectLoader, isValidUrl, OperatorList, OPS */
 
@@ -266,7 +266,7 @@ var Annotation = (function AnnotationClosure() {
     if (annotation.isViewable()) {
       return annotation;
     } else {
-      TODO('unimplemented annotation type: ' + subtype);
+      warn('unimplemented annotation type: ' + subtype);
     }
   };
 
@@ -358,7 +358,7 @@ var WidgetAnnotation = (function WidgetAnnotationClosure() {
   Util.inherit(WidgetAnnotation, Annotation, {
     isViewable: function WidgetAnnotation_isViewable() {
       if (this.data.fieldType === 'Sig') {
-        TODO('unimplemented annotation type: Widget signature');
+        warn('unimplemented annotation type: Widget signature');
         return false;
       }
 
@@ -643,7 +643,7 @@ var LinkAnnotation = (function LinkAnnotationClosure() {
       } else if (linkType === 'Named') {
         data.action = action.get('N').name;
       } else {
-        TODO('unrecognized link type: ' + linkType);
+        warn('unrecognized link type: ' + linkType);
       }
     } else if (dict.has('Dest')) {
       // simple destination link
diff --git a/src/shared/colorspace.js b/src/shared/colorspace.js
index 62f7a4d4f..9eedfc8e7 100644
--- a/src/shared/colorspace.js
+++ b/src/shared/colorspace.js
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 /* globals error, info, isArray, isDict, isName, isStream, isString,
-           PDFFunction, warn, shadow, TODO */
+           PDFFunction, warn, shadow */
 
 'use strict';
 
@@ -671,7 +671,7 @@ var CalGrayCS = (function CalGrayCSClosure() {
     }
 
     if (this.XB !== 0 || this.YB !== 0 || this.ZB !== 0) {
-      TODO(this.name + ', BlackPoint: XB: ' + this.XB + ', YB: ' + this.YB +
+      warn(this.name + ', BlackPoint: XB: ' + this.XB + ', YB: ' + this.YB +
            ', ZB: ' + this.ZB + ', only default values are supported.');
     }
 
diff --git a/src/shared/function.js b/src/shared/function.js
index 0606bac46..15b153957 100644
--- a/src/shared/function.js
+++ b/src/shared/function.js
@@ -1,6 +1,5 @@
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
-/* globals EOF, error, isArray, isBool, Lexer, TODO */
 /* Copyright 2012 Mozilla Foundation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+/* globals EOF, error, isArray, isBool, Lexer, info */
 
 'use strict';
 
@@ -122,7 +122,7 @@ var PDFFunction = (function PDFFunctionClosure() {
       if (order !== 1) {
         // No description how cubic spline interpolation works in PDF32000:2008
         // As in poppler, ignoring order, linear interpolation may work as good
-        TODO('No support for cubic spline interpolation: ' + order);
+        info('No support for cubic spline interpolation: ' + order);
       }
 
       var encode = dict.get('Encode');
diff --git a/src/shared/pattern.js b/src/shared/pattern.js
index b2c5456fb..f0fa153a8 100644
--- a/src/shared/pattern.js
+++ b/src/shared/pattern.js
@@ -15,8 +15,8 @@
  * limitations under the License.
  */
 /* globals CanvasGraphics, ColorSpace, DeviceRgbCS, error,
-           info, isArray, isPDFFunction, isStream, PDFFunction, TODO, Util,
-           warn, CachedCanvases */
+           info, isArray, isPDFFunction, isStream, PDFFunction, Util,
+           warn, CachedCanvases, UnsupportedManager, UNSUPPORTED_FEATURES */
 
 'use strict';
 
@@ -55,7 +55,7 @@ var Pattern = (function PatternClosure() {
         // Both radial and axial shadings are handled by RadialAxial shading.
         return new Shadings.RadialAxial(dict, matrix, xref, res);
       default:
-        TODO('Unsupported shading type: ' + type);
+        UnsupportedManager.notify(UNSUPPORTED_FEATURES.shadingPattern);
         return new Shadings.Dummy();
     }
   };
@@ -315,7 +315,7 @@ var TilingPattern = (function TilingPatternClosure() {
       var commonObjs = this.commonObjs;
       var ctx = this.ctx;
 
-      TODO('TilingType: ' + tilingType);
+      info('TilingType: ' + tilingType);
 
       var x0 = bbox[0], y0 = bbox[1], x1 = bbox[2], y1 = bbox[3];
 
diff --git a/src/shared/util.js b/src/shared/util.js
index 598674990..00b62a0d2 100644
--- a/src/shared/util.js
+++ b/src/shared/util.js
@@ -156,21 +156,19 @@ var log = (function() {
   }
 })();
 
-// A notice for devs that will not trigger the fallback UI.  These are good
-// for things that are helpful to devs, such as warning that Workers were
-// disabled, which is important to devs but not end users.
+// A notice for devs. These are good for things that are helpful to devs, such
+// as warning that Workers were disabled, which is important to devs but not
+// end users.
 function info(msg) {
   if (PDFJS.verbosity >= PDFJS.VERBOSITY_LEVELS.infos) {
     log('Info: ' + msg);
-    PDFJS.LogManager.notify('info', msg);
   }
 }
 
-// Non-fatal warnings that should trigger the fallback UI.
+// Non-fatal warnings.
 function warn(msg) {
   if (PDFJS.verbosity >= PDFJS.VERBOSITY_LEVELS.warnings) {
     log('Warning: ' + msg);
-    PDFJS.LogManager.notify('warn', msg);
   }
 }
 
@@ -188,15 +186,10 @@ function error(msg) {
     log('Error: ' + msg);
   }
   log(backtrace());
-  PDFJS.LogManager.notify('error', msg);
+  UnsupportedManager.notify(UNSUPPORTED_FEATURES.unknown);
   throw new Error(msg);
 }
 
-// Missing features that should trigger the fallback UI.
-function TODO(what) {
-  warn('TODO: ' + what);
-}
-
 function backtrace() {
   try {
     throw new Error();
@@ -210,6 +203,31 @@ function assert(cond, msg) {
     error(msg);
 }
 
+var UNSUPPORTED_FEATURES = PDFJS.UNSUPPORTED_FEATURES = {
+  unknown: 'unknown',
+  forms: 'forms',
+  javaScript: 'javaScript',
+  smask: 'smask',
+  shadingPattern: 'shadingPattern',
+  font: 'font'
+};
+
+var UnsupportedManager = PDFJS.UnsupportedManager =
+  (function UnsupportedManagerClosure() {
+  var listeners = [];
+  return {
+    listen: function (cb) {
+      listeners.push(cb);
+    },
+    notify: function (featureId) {
+      warn('Unsupported feature "' + featureId + '"');
+      for (var i = 0, ii = listeners.length; i < ii; i++) {
+        listeners[i](featureId);
+      }
+    }
+  };
+})();
+
 // Combines two URLs. The baseUrl shall be absolute URL. If the url is an
 // absolute URL, it will be returned as is.
 function combineUrl(baseUrl, url) {
@@ -263,22 +281,6 @@ function assertWellFormed(cond, msg) {
     error(msg);
 }
 
-var LogManager = PDFJS.LogManager = (function LogManagerClosure() {
-  var loggers = [];
-  return {
-    addLogger: function logManager_addLogger(logger) {
-      loggers.push(logger);
-    },
-    notify: function(type, message) {
-      for (var i = 0, ii = loggers.length; i < ii; i++) {
-        var logger = loggers[i];
-        if (logger[type])
-          logger[type](message);
-      }
-    }
-  };
-})();
-
 function shadow(obj, prop, value) {
   Object.defineProperty(obj, prop, { value: value,
                                      enumerable: true,
@@ -1152,8 +1154,8 @@ function MessageHandler(name, comObj) {
       log.apply(null, data);
     }];
   }
-  ah['_warn'] = [function ah_Warn(data) {
-    warn(data);
+  ah['_unsupported_feature'] = [function ah_unsupportedFeature(data) {
+    UnsupportedManager.notify(data);
   }];
 
   comObj.onmessage = function messageHandlerComObjOnMessage(event) {
diff --git a/web/viewer.js b/web/viewer.js
index 1ded06cf9..f753e3da4 100644
--- a/web/viewer.js
+++ b/web/viewer.js
@@ -599,7 +599,7 @@ var PDFView = {
     ).then(null, noData);
   },
 
-  fallback: function pdfViewFallback() {
+  fallback: function pdfViewFallback(featureId) {
 //#if !(FIREFOX || MOZCENTRAL)
 //  return;
 //#else
@@ -935,7 +935,7 @@ var PDFView = {
         pdfDocument.getJavaScript().then(function(javaScript) {
           if (javaScript.length) {
             console.warn('Warning: JavaScript is not supported');
-            PDFView.fallback();
+            PDFView.fallback(PDFJS.UNSUPPORTED_FEATURES.javaScript);
           }
           // Hack to support auto printing.
           var regex = /\bprint\s*\(/g;
@@ -1000,7 +1000,7 @@ var PDFView = {
 
       if (info.IsAcroFormPresent) {
         console.warn('Warning: AcroForm/XFA is not supported');
-        PDFView.fallback();
+        PDFView.fallback(PDFJS.UNSUPPORTED_FEATURES.forms);
       }
 
 //#if (FIREFOX || MOZCENTRAL)
@@ -1661,13 +1661,8 @@ document.addEventListener('DOMContentLoaded', function webViewerLoad(evt) {
     document.getElementById('viewFind').classList.add('hidden');
   }
 
-  // Listen for warnings to trigger the fallback UI.  Errors should be caught
-  // and call PDFView.error() so we don't need to listen for those.
-  PDFJS.LogManager.addLogger({
-    warn: function() {
-      PDFView.fallback();
-    }
-  });
+  // Listen for unsuporrted features to trigger the fallback UI.
+  PDFJS.UnsupportedManager.listen(PDFView.fallback.bind(PDFView));
 
   // Suppress context menus for some controls
   document.getElementById('scaleSelect').oncontextmenu = noContextMenuHandler;