diff --git a/src/error/PhutilErrorHandler.php b/src/error/PhutilErrorHandler.php
index 63d5b492..4439bfaf 100644
--- a/src/error/PhutilErrorHandler.php
+++ b/src/error/PhutilErrorHandler.php
@@ -1,612 +1,618 @@
 <?php
 
 /**
  * Improve PHP error logs and optionally route errors, exceptions and debugging
  * information to a central listener.
  *
  * This class takes over the PHP error and exception handlers when you call
  * ##PhutilErrorHandler::initialize()## and forwards all debugging information
  * to a listener you install with ##PhutilErrorHandler::setErrorListener()##.
  *
  * To use PhutilErrorHandler, which will enhance the messages printed to the
  * PHP error log, just initialize it:
  *
  *    PhutilErrorHandler::initialize();
  *
  * To additionally install a custom listener which can print error information
  * to some other file or console, register a listener:
  *
  *    PhutilErrorHandler::setErrorListener($some_callback);
  *
  * For information on writing an error listener, see
  * @{function:phutil_error_listener_example}. Providing a listener is optional,
  * you will benefit from improved error logs even without one.
  *
  * Phabricator uses this class to drive the DarkConsole "Error Log" plugin.
  *
  * @task config   Configuring Error Dispatch
  * @task exutil   Exception Utilities
  * @task trap     Error Traps
  * @task internal Internals
  */
 final class PhutilErrorHandler extends Phobject {
 
   private static $errorListener = null;
   private static $initialized = false;
   private static $traps = array();
 
   const EXCEPTION   = 'exception';
   const ERROR       = 'error';
   const PHLOG       = 'phlog';
   const DEPRECATED  = 'deprecated';
 
 
 /* -(  Configuring Error Dispatch  )----------------------------------------- */
 
 
   /**
    * Registers this class as the PHP error and exception handler. This will
    * overwrite any previous handlers!
    *
    * @return void
    * @task config
    */
   public static function initialize() {
     self::$initialized = true;
     set_error_handler(array(__CLASS__, 'handleError'));
     set_exception_handler(array(__CLASS__, 'handleException'));
   }
 
   /**
    * Provide an optional listener callback which will receive all errors,
    * exceptions and debugging messages. It can then print them to a web console,
    * for example.
    *
    * See @{function:phutil_error_listener_example} for details about the
    * callback parameters and operation.
    *
    * @return void
    * @task config
    */
   public static function setErrorListener($listener) {
     self::$errorListener = $listener;
   }
 
 
 /* -(  Exception Utilities  )------------------------------------------------ */
 
 
   /**
    * Gets the previous exception of a nested exception. Prior to PHP 5.3 you
    * can use @{class:PhutilProxyException} to nest exceptions; after PHP 5.3
    * all exceptions are nestable.
    *
    * @param   Exception|Throwable       Exception to unnest.
    * @return  Exception|Throwable|null  Previous exception, if one exists.
    * @task    exutil
    */
   public static function getPreviousException($ex) {
     if (method_exists($ex, 'getPrevious')) {
       return $ex->getPrevious();
     }
     if (method_exists($ex, 'getPreviousException')) {
       return $ex->getPreviousException();
     }
     return null;
   }
 
 
   /**
    * Find the most deeply nested exception from a possibly-nested exception.
    *
    * @param   Exception|Throwable     A possibly-nested exception.
    * @return  Exception|Throwable     Deepest exception in the nest.
    * @task    exutil
    */
   public static function getRootException($ex) {
     $root = $ex;
     while (self::getPreviousException($root)) {
       $root = self::getPreviousException($root);
     }
     return $root;
   }
 
 
 /* -(  Trapping Errors  )---------------------------------------------------- */
 
 
   /**
    * Adds an error trap. Normally you should not invoke this directly;
    * @{class:PhutilErrorTrap} registers itself on construction.
    *
    * @param PhutilErrorTrap Trap to add.
    * @return void
    * @task trap
    */
   public static function addErrorTrap(PhutilErrorTrap $trap) {
     $key = $trap->getTrapKey();
     self::$traps[$key] = $trap;
   }
 
 
   /**
    * Removes an error trap. Normally you should not invoke this directly;
    * @{class:PhutilErrorTrap} deregisters itself on destruction.
    *
    * @param PhutilErrorTrap Trap to remove.
    * @return void
    * @task trap
    */
   public static function removeErrorTrap(PhutilErrorTrap $trap) {
     $key = $trap->getTrapKey();
     unset(self::$traps[$key]);
   }
 
 
 /* -(  Internals  )---------------------------------------------------------- */
 
 
   /**
    * Determine if PhutilErrorHandler has been initialized.
    *
    * @return bool True if initialized.
    * @task internal
    */
   public static function hasInitialized() {
     return self::$initialized;
   }
 
 
   /**
    * Handles PHP errors and dispatches them forward. This is a callback for
    * ##set_error_handler()##. You should not call this function directly; use
    * @{function:phlog} to print debugging messages or ##trigger_error()## to
    * trigger PHP errors.
    *
    * This handler converts E_RECOVERABLE_ERROR messages from violated typehints
    * into @{class:InvalidArgumentException}s.
    *
    * This handler converts other E_RECOVERABLE_ERRORs into
    * @{class:RuntimeException}s.
    *
    * This handler converts E_NOTICE messages from uses of undefined variables
    * into @{class:RuntimeException}s.
    *
    * @param int Error code.
    * @param string Error message.
    * @param string File where the error occurred.
    * @param int Line on which the error occurred.
    * @param wild Error context information.
    * @return void
    * @task internal
    */
   public static function handleError($num, $str, $file, $line, $ctx = null) {
     foreach (self::$traps as $trap) {
       $trap->addError($num, $str, $file, $line);
     }
 
     if ((error_reporting() & $num) == 0) {
       // Respect the use of "@" to silence warnings: if this error was
       // emitted from a context where "@" was in effect, the
       // value returned by error_reporting() will be 0. This is the
       // recommended way to check for this, see set_error_handler() docs
       // on php.net.
       return false;
     }
 
     // See T13499. If this is a user error arising from "trigger_error()" or
     // similar, route it through normal error handling: this is probably the
     // best match to authorial intent, since the code could choose to throw
     // an exception instead if it wanted that behavior. Phabricator does not
     // use "trigger_error()" so we never normally expect to reach this
     // block in first-party code.
 
     if (($num === E_USER_ERROR) ||
         ($num === E_USER_WARNING) ||
-        ($num === E_USER_NOTICE)) {
+        ($num === E_USER_NOTICE) ||
+        ($num === E_DEPRECATED)) {
+
+      // See T15554 - we special-case E_DEPRECATED because we don't want them
+      // to kill the process.
+      $level = ($num === E_DEPRECATED) ? self::DEPRECATED : self::ERROR;
 
       $trace = debug_backtrace();
       array_shift($trace);
       self::dispatchErrorMessage(
-        self::ERROR,
+        $level,
         $str,
         array(
           'file'       => $file,
           'line'       => $line,
           'error_code' => $num,
           'trace'      => $trace,
         ));
 
       return;
     }
 
     // Convert typehint failures into exceptions.
     if (preg_match('/^Argument (\d+) passed to (\S+) must be/', $str)) {
       throw new InvalidArgumentException($str);
     }
 
     // Convert other E_RECOVERABLE_ERRORs into generic runtime exceptions.
     if ($num == E_RECOVERABLE_ERROR) {
       throw new RuntimeException($str);
     }
 
     // Convert uses of undefined variables into exceptions.
     if (preg_match('/^Undefined variable: /', $str)) {
       throw new RuntimeException($str);
     }
 
     // Convert uses of undefined properties into exceptions.
     if (preg_match('/^Undefined property: /', $str)) {
       throw new RuntimeException($str);
     }
 
     // Convert undefined constants into exceptions. Usually this means there
     // is a missing `$` and the program is horribly broken.
     if (preg_match('/^Use of undefined constant /', $str)) {
       throw new RuntimeException($str);
     }
 
     // Convert undefined indexes into exceptions.
     if (preg_match('/^Undefined index: /', $str)) {
       throw new RuntimeException($str);
     }
 
     // Convert undefined offsets into exceptions.
     if (preg_match('/^Undefined offset: /', $str)) {
       throw new RuntimeException($str);
     }
 
     // See T13499. Convert all other runtime errors not handled in a more
     // specific way into runtime exceptions.
     throw new RuntimeException($str);
   }
 
   /**
    * Handles PHP exceptions and dispatches them forward. This is a callback for
    * ##set_exception_handler()##. You should not call this function directly;
    * to print exceptions, pass the exception object to @{function:phlog}.
    *
    * @param Exception|Throwable Uncaught exception object.
    * @return void
    * @task internal
    */
   public static function handleException($ex) {
     self::dispatchErrorMessage(
       self::EXCEPTION,
       $ex,
       array(
         'file'  => $ex->getFile(),
         'line'  => $ex->getLine(),
         'trace' => self::getExceptionTrace($ex),
         'catch_trace' => debug_backtrace(),
       ));
 
     // Normally, PHP exits with code 255 after an uncaught exception is thrown.
     // However, if we install an exception handler (as we have here), it exits
     // with code 0 instead. Script execution terminates after this function
     // exits in either case, so exit explicitly with the correct exit code.
     exit(255);
   }
 
 
   /**
    * Output a stacktrace to the PHP error log.
    *
    * @param trace A stacktrace, e.g. from debug_backtrace();
    * @return void
    * @task internal
    */
   public static function outputStacktrace($trace) {
     $lines = explode("\n", self::formatStacktrace($trace));
     foreach ($lines as $line) {
       error_log($line);
     }
   }
 
 
   /**
    * Format a stacktrace for output.
    *
    * @param trace A stacktrace, e.g. from debug_backtrace();
    * @return string Human-readable trace.
    * @task internal
    */
   public static function formatStacktrace($trace) {
     $result = array();
 
     $libinfo = self::getLibraryVersions();
     if ($libinfo) {
       foreach ($libinfo as $key => $dict) {
         $info = array();
         foreach ($dict as $dkey => $dval) {
           $info[] = $dkey.'='.$dval;
         }
         $libinfo[$key] = $key.'('.implode(', ', $info).')';
       }
       $result[] = implode(', ', $libinfo);
     }
 
     foreach ($trace as $key => $entry) {
       $line = '  #'.$key.' ';
       if (!empty($entry['xid'])) {
         if ($entry['xid'] != 1) {
           $line .= '<#'.$entry['xid'].'> ';
         }
       }
       if (isset($entry['class'])) {
         $line .= $entry['class'].'::';
       }
       $line .= idx($entry, 'function', '');
 
       if (isset($entry['args'])) {
         $args = array();
         foreach ($entry['args'] as $arg) {
 
           // NOTE: Print out object types, not values. Values sometimes contain
           // sensitive information and are usually not particularly helpful
           // for debugging.
 
           $type = (gettype($arg) == 'object')
             ? get_class($arg)
             : gettype($arg);
           $args[] = $type;
         }
         $line .= '('.implode(', ', $args).')';
       }
 
       if (isset($entry['file'])) {
         $file = self::adjustFilePath($entry['file']);
         $line .= ' called at ['.$file.':'.$entry['line'].']';
       }
 
       $result[] = $line;
     }
     return implode("\n", $result);
   }
 
 
   /**
    * All different types of error messages come here before they are
    * dispatched to the listener; this method also prints them to the PHP error
    * log.
    *
    * @param const Event type constant.
    * @param wild Event value.
    * @param dict Event metadata.
    * @return void
    * @task internal
    */
   public static function dispatchErrorMessage($event, $value, $metadata) {
     $timestamp = date('Y-m-d H:i:s');
 
     switch ($event) {
+      case self::DEPRECATED:
       case self::ERROR:
         $default_message = sprintf(
           '[%s] ERROR %d: %s at [%s:%d]',
           $timestamp,
           $metadata['error_code'],
           $value,
           $metadata['file'],
           $metadata['line']);
 
         $metadata['default_message'] = $default_message;
         error_log($default_message);
         self::outputStacktrace($metadata['trace']);
         break;
       case self::EXCEPTION:
         $messages = array();
         $current = $value;
         do {
           $messages[] = '('.get_class($current).') '.$current->getMessage();
         } while ($current = self::getPreviousException($current));
         $messages = implode(' {>} ', $messages);
 
         if (strlen($messages) > 4096) {
           $messages = substr($messages, 0, 4096).'...';
         }
 
         $default_message = sprintf(
           '[%s] EXCEPTION: %s at [%s:%d]',
           $timestamp,
           $messages,
           self::adjustFilePath(self::getRootException($value)->getFile()),
           self::getRootException($value)->getLine());
 
         $metadata['default_message'] = $default_message;
         error_log($default_message);
         self::outputStacktrace($metadata['trace']);
         break;
       case self::PHLOG:
         $default_message = sprintf(
           '[%s] PHLOG: %s at [%s:%d]',
           $timestamp,
           PhutilReadableSerializer::printShort($value),
           $metadata['file'],
           $metadata['line']);
 
         $metadata['default_message'] = $default_message;
         error_log($default_message);
         break;
       default:
         error_log(pht('Unknown event %s', $event));
         break;
     }
 
     if (self::$errorListener) {
       static $handling_error;
       if ($handling_error) {
         error_log(
           'Error handler was reentered, some errors were not passed to the '.
           'listener.');
         return;
       }
       $handling_error = true;
       call_user_func(self::$errorListener, $event, $value, $metadata);
       $handling_error = false;
     }
   }
 
   public static function adjustFilePath($path) {
     // Compute known library locations so we can emit relative paths if the
     // file resides inside a known library. This is a little cleaner to read,
     // and limits the number of false positives we get about full path
     // disclosure via HackerOne.
 
     $bootloader = PhutilBootloader::getInstance();
     $libraries = $bootloader->getAllLibraries();
     $roots = array();
     foreach ($libraries as $library) {
       $root = $bootloader->getLibraryRoot($library);
       // For these libraries, the effective root is one level up.
       switch ($library) {
         case 'arcanist':
         case 'phorge':
         case 'phabricator':
           $root = dirname($root);
           break;
       }
 
       if (!strncmp($root, $path, strlen($root))) {
         return '<'.$library.'>'.substr($path, strlen($root));
       }
     }
 
     return $path;
   }
 
   public static function getLibraryVersions() {
     $libinfo = array();
 
     $bootloader = PhutilBootloader::getInstance();
     foreach ($bootloader->getAllLibraries() as $library) {
       $root = phutil_get_library_root($library);
       $try_paths = array(
         $root,
         dirname($root),
       );
       $libinfo[$library] = array();
 
       $get_refs = array('master');
       foreach ($try_paths as $try_path) {
         // Try to read what the HEAD of the repository is pointed at. This is
         // normally the name of a branch ("ref").
         $try_file = $try_path.'/.git/HEAD';
         if (@file_exists($try_file)) {
           $head = @file_get_contents($try_file);
           $matches = null;
           if (preg_match('(^ref: refs/heads/(.*)$)', trim($head), $matches)) {
             $libinfo[$library]['head'] = trim($matches[1]);
             $get_refs[] = trim($matches[1]);
           } else {
             $libinfo[$library]['head'] = trim($head);
           }
           break;
         }
       }
 
       // Try to read which commit relevant branch heads are at.
       foreach (array_unique($get_refs) as $ref) {
         foreach ($try_paths as $try_path) {
           $try_file = $try_path.'/.git/refs/heads/'.$ref;
           if (@file_exists($try_file)) {
             $hash = @file_get_contents($try_file);
             if ($hash) {
               $libinfo[$library]['ref.'.$ref] = substr(trim($hash), 0, 12);
               break;
             }
           }
         }
       }
 
       // Look for extension files.
       $custom = @scandir($root.'/extensions/');
       if ($custom) {
         $count = 0;
         foreach ($custom as $custom_path) {
           if (preg_match('/\.php$/', $custom_path)) {
             $count++;
           }
         }
         if ($count) {
           $libinfo[$library]['custom'] = $count;
         }
       }
     }
 
     ksort($libinfo);
 
     return $libinfo;
   }
 
   /**
    * Get a full trace across all proxied and aggregated exceptions.
    *
    * This attempts to build a set of stack frames which completely represent
    * all of the places an exception came from, even if it came from multiple
    * origins and has been aggregated or proxied.
    *
    * @param Exception|Throwable Exception to retrieve a trace for.
    * @return list<wild> List of stack frames.
    */
   public static function getExceptionTrace($ex) {
     $id = 1;
 
     // Keep track of discovered exceptions which we need to build traces for.
     $stack = array(
       array($id, $ex),
     );
 
     $frames = array();
     while ($info = array_shift($stack)) {
       list($xid, $ex) = $info;
 
       // We're going from top-level exception down in bredth-first order, but
       // want to build a trace in approximately standard order (deepest part of
       // the call stack to most shallow) so we need to reverse each list of
       // frames and then reverse everything at the end.
 
       $ex_frames = array_reverse($ex->getTrace());
       $ex_frames = array_values($ex_frames);
       $last_key = (count($ex_frames) - 1);
       foreach ($ex_frames as $frame_key => $frame) {
         $frame['xid'] = $xid;
 
         // If this is a child/previous exception and we're on the deepest frame
         // and missing file/line data, fill it in from the exception itself.
         if ($xid > 1 && ($frame_key == $last_key)) {
           if (empty($frame['file'])) {
             $frame['file'] = $ex->getFile();
             $frame['line'] = $ex->getLine();
           }
         }
 
         // Since the exceptions are likely to share the most shallow frames,
         // try to add those to the trace only once.
         if (isset($frame['file']) && isset($frame['line'])) {
           $signature = $frame['file'].':'.$frame['line'];
           if (empty($frames[$signature])) {
             $frames[$signature] = $frame;
           }
         } else {
           $frames[] = $frame;
         }
       }
 
       // If this is a proxy exception, add the proxied exception.
       $prev = self::getPreviousException($ex);
       if ($prev) {
         $stack[] = array(++$id, $prev);
       }
 
       // If this is an aggregate exception, add the child exceptions.
       if ($ex instanceof PhutilAggregateException) {
         foreach ($ex->getExceptions() as $child) {
           $stack[] = array(++$id, $child);
         }
       }
     }
 
     return array_values(array_reverse($frames));
   }
 
 }