Refactoring error messages. I changed 'em from public static final constants to methods. The benefit is that now the arguments are typechecked. More importantly, now we can gather coverage on methods. Which opens the door to figuring out which methods we have test coverage for (not very many!)

git-svn-id: https://google-guice.googlecode.com/svn/trunk@516 d779f126-a31b-0410-b53b-1d3aecad763e
diff --git a/src/com/google/inject/BindCommandProcessor.java b/src/com/google/inject/BindCommandProcessor.java
index 6dec2a6..ae78278 100644
--- a/src/com/google/inject/BindCommandProcessor.java
+++ b/src/com/google/inject/BindCommandProcessor.java
@@ -17,18 +17,18 @@
 package com.google.inject;
 
 import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
 import com.google.inject.commands.BindCommand;
 import com.google.inject.commands.BindConstantCommand;
 import com.google.inject.commands.BindScoping.Visitor;
 import com.google.inject.commands.BindTarget;
 import com.google.inject.internal.Annotations;
-import com.google.inject.internal.ErrorMessages;
+import com.google.inject.internal.ErrorMessage;
 import com.google.inject.internal.ResolveFailedException;
 import com.google.inject.internal.StackTraceElements;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -66,7 +66,7 @@
     Class<? super T> rawType = key.getTypeLiteral().getRawType();
 
     if (rawType == Provider.class) {
-      addError(source, ErrorMessages.BINDING_TO_PROVIDER);
+      addError(source, ErrorMessage.bindingToProvider());
       return true;
     }
 
@@ -89,8 +89,8 @@
         if (scope != null) {
           return scope;
         } else {
-          addError(source, ErrorMessages.SCOPE_NOT_FOUND,
-              "@" + scopeAnnotation.getSimpleName());
+          addError(source, ErrorMessage.scopeNotFound(
+              "@" + scopeAnnotation.getSimpleName()));
           return Scopes.NO_SCOPE;
         }
       }
@@ -135,7 +135,7 @@
 
       public Void visitToKey(Key<? extends T> targetKey) {
         if (key.equals(targetKey)) {
-          addError(source, ErrorMessages.RECURSIVE_BINDING);
+          addError(source, ErrorMessage.recursiveBinding());
         }
 
         FactoryProxy<T> factory = new FactoryProxy<T>(key, targetKey, source);
@@ -155,7 +155,7 @@
         // We can't assume abstract types aren't injectable. They may have an
         // @ImplementedBy annotation or something.
         if (key.hasAnnotationType() || !(type instanceof Class<?>)) {
-          addError(source, ErrorMessages.MISSING_IMPLEMENTATION);
+          addError(source, ErrorMessage.missingImplementation());
           putBinding(invalidBinding(injector, key, source));
           return null;
         }
@@ -168,7 +168,7 @@
           binding = injector.createUnitializedBinding(clazz, scope, source, loadStrategy);
           putBinding(binding);
         } catch (ResolveFailedException e) {
-          injector.errorHandler.handle(source, e.getMessage());
+          injector.errorHandler.handle(e.getMessage(source));
           putBinding(invalidBinding(injector, key, source));
           return null;
         }
@@ -178,7 +178,7 @@
             try {
               injector.initializeBinding(binding);
             } catch (ResolveFailedException e) {
-              injector.errorHandler.handle(source, e.getMessage());
+              injector.errorHandler.handle(e.getMessage(source));
             }
           }
         });
@@ -196,12 +196,12 @@
 
       if (!Annotations.isRetainedAtRuntime(annotationType)) {
         addError(StackTraceElements.forType(annotationType),
-            ErrorMessages.MISSING_RUNTIME_RETENTION, source);
+            ErrorMessage.missingRuntimeRetention(source));
       }
 
       if (!Key.isBindingAnnotation(annotationType)) {
         addError(StackTraceElements.forType(annotationType),
-            ErrorMessages.MISSING_BINDING_ANNOTATION, source);
+            ErrorMessage.missingBindingAnnotation(source));
       }
     }
   }
@@ -213,7 +213,7 @@
   @Override public Boolean visitBindConstant(BindConstantCommand command) {
     BindTarget<?> target = command.getTarget();
     if (target == null) {
-      addError(command.getSource(), ErrorMessages.MISSING_CONSTANT_VALUE);
+      addError(command.getSource(), ErrorMessage.missingConstantValues());
       return true;
     }
 
@@ -244,14 +244,14 @@
 
     Class<?> rawType = key.getRawType();
     if (FORBIDDEN_TYPES.contains(rawType)) {
-      addError(binding.getSource(), ErrorMessages.CANNOT_BIND_TO_GUICE_TYPE,
-          rawType.getSimpleName());
+      addError(binding.getSource(), ErrorMessage.cannotBindToGuiceType(
+          rawType.getSimpleName()));
       return;
     }
 
     if (bindings.containsKey(key)) {
-      addError(binding.getSource(), ErrorMessages.BINDING_ALREADY_SET, key,
-          original.getSource());
+      addError(binding.getSource(), ErrorMessage.bindingAlreadySet(key,
+          original.getSource()));
     } else {
       bindings.put(key, binding);
     }
@@ -261,7 +261,7 @@
 
   @SuppressWarnings("unchecked") // For generic array creation.
   private static Set<Class<?>> forbiddenTypes() {
-    Set<Class<?>> set = new HashSet<Class<?>>();
+    Set<Class<?>> set = Sets.newHashSet();
 
     Collections.addAll(set,
 
diff --git a/src/com/google/inject/BoundProviderFactory.java b/src/com/google/inject/BoundProviderFactory.java
index 3cbfc11..126c5a3 100644
--- a/src/com/google/inject/BoundProviderFactory.java
+++ b/src/com/google/inject/BoundProviderFactory.java
@@ -17,7 +17,7 @@
 package com.google.inject;
 
 import com.google.inject.BindCommandProcessor.CreationListener;
-import com.google.inject.internal.ErrorMessages;
+import com.google.inject.internal.ErrorMessage;
 import com.google.inject.internal.ResolveFailedException;
 
 /**
@@ -43,7 +43,7 @@
         try {
           providerFactory = injector.getInternalFactory(providerKey);
         } catch (ResolveFailedException e) {
-          injector.errorHandler.handle(source, e.getMessage());
+          injector.errorHandler.handle(e.getMessage(source));
         }
       }
     });
@@ -56,7 +56,7 @@
     } catch(ProvisionException e) {
       throw e;
     } catch(RuntimeException e) {
-      throw new ProvisionException(e, ErrorMessages.ERROR_IN_PROVIDER);
+      throw new ProvisionException(ErrorMessage.errorInProvider().toString(), e);
     }
   }
 
diff --git a/src/com/google/inject/CommandProcessor.java b/src/com/google/inject/CommandProcessor.java
index d92f507..1b558a8 100644
--- a/src/com/google/inject/CommandProcessor.java
+++ b/src/com/google/inject/CommandProcessor.java
@@ -16,9 +16,18 @@
 
 package com.google.inject;
 
-import com.google.inject.commands.*;
+import com.google.inject.commands.AddMessageErrorCommand;
+import com.google.inject.commands.AddThrowableErrorCommand;
+import com.google.inject.commands.BindCommand;
+import com.google.inject.commands.BindConstantCommand;
+import com.google.inject.commands.BindInterceptorCommand;
+import com.google.inject.commands.BindScopeCommand;
+import com.google.inject.commands.Command;
+import com.google.inject.commands.ConvertToTypesCommand;
+import com.google.inject.commands.GetProviderCommand;
+import com.google.inject.commands.RequestStaticInjectionCommand;
 import com.google.inject.internal.ErrorHandler;
-
+import com.google.inject.internal.ErrorMessage;
 import java.util.Iterator;
 import java.util.List;
 
@@ -33,7 +42,7 @@
  */
 abstract class CommandProcessor implements Command.Visitor<Boolean> {
 
-  private final ErrorHandler errorHandler;
+  protected final ErrorHandler errorHandler;
 
   protected CommandProcessor(ErrorHandler errorHandler) {
     this.errorHandler = errorHandler;
@@ -48,12 +57,8 @@
     }
   }
 
-  protected void addError(Object source, String message, Object... arguments) {
-    errorHandler.handle(source, message, arguments);
-  }
-
-  protected void addError(Object source, String message) {
-    errorHandler.handle(source, message);
+  protected void addError(Object source, ErrorMessage errorMessage) {
+    errorHandler.handle(source, errorMessage);
   }
 
   public Boolean visitAddMessageError(AddMessageErrorCommand command) {
diff --git a/src/com/google/inject/ConfigurationException.java b/src/com/google/inject/ConfigurationException.java
deleted file mode 100644
index 6e0dd09..0000000
--- a/src/com/google/inject/ConfigurationException.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * Copyright (C) 2006 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.inject;
-
-/**
- * Thrown when the {@link InjectorBuilder} is misconfigured.
- *
- * @author crazybob@google.com (Bob Lee)
- */
-class ConfigurationException extends RuntimeException {
-
-  ConfigurationException(String message) {
-    super(message);
-  }
-}
diff --git a/src/com/google/inject/ConstructionContext.java b/src/com/google/inject/ConstructionContext.java
index 817fc15..0ce62d9 100644
--- a/src/com/google/inject/ConstructionContext.java
+++ b/src/com/google/inject/ConstructionContext.java
@@ -16,6 +16,8 @@
 
 package com.google.inject;
 
+import com.google.inject.internal.ErrorMessage;
+import com.google.inject.spi.Message;
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
@@ -67,9 +69,8 @@
 
     if (!expectedType.isInterface()) {
       // TODO: Report better error.
-      throw new ConfigurationException("Tried proxying "
-          + expectedType.getName() + " to support a circular dependency, but"
-          + " it is not an interface.");
+      throw new CreationException(
+          new Message(ErrorMessage.cannotSatisfyCircularDependency(expectedType).toString()));
     }
 
     if (invocationHandlers == null) {
diff --git a/src/com/google/inject/ConstructorInjector.java b/src/com/google/inject/ConstructorInjector.java
index 32732d5..06a272b 100644
--- a/src/com/google/inject/ConstructorInjector.java
+++ b/src/com/google/inject/ConstructorInjector.java
@@ -18,9 +18,8 @@
 
 import com.google.inject.InjectorImpl.SingleMemberInjector;
 import com.google.inject.InjectorImpl.SingleParameterInjector;
-import com.google.inject.internal.ErrorMessages;
+import com.google.inject.internal.ErrorMessage;
 import com.google.inject.internal.ResolveFailedException;
-
 import java.lang.reflect.InvocationTargetException;
 import java.util.List;
 
@@ -55,7 +54,7 @@
               constructionProxy.getParameters());
     }
     catch (ResolveFailedException e) {
-      injector.errorHandler.handle(constructionProxy.getMember(), e.getMessage());
+      injector.errorHandler.handle(e.getMessage(constructionProxy.getMember()));
       return null;
     }
   }
@@ -108,8 +107,7 @@
     }
     catch (InvocationTargetException e) {
       Throwable cause = e.getCause() != null ? e.getCause() : e;
-      throw new ProvisionException(cause,
-          ErrorMessages.ERROR_INJECTING_CONSTRUCTOR);
+      throw new ProvisionException(ErrorMessage.errorInjectingConstructor().toString(), cause);
     }
     finally {
       constructionContext.removeCurrentReference();
diff --git a/src/com/google/inject/CreationException.java b/src/com/google/inject/CreationException.java
index ba78de4..1620548 100644
--- a/src/com/google/inject/CreationException.java
+++ b/src/com/google/inject/CreationException.java
@@ -17,13 +17,17 @@
 package com.google.inject;
 
 import com.google.inject.spi.Message;
-
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Formatter;
+import java.util.List;
 
 /**
- * Thrown when errors occur while creating a {@link Injector}. Includes a list
- * of encountered errors. Typically, a client should catch this exception, log
- * it, and stop execution.
+ * Thrown when errors occur when configuration problems are encountered when
+ * creating or using an {@link Injector}. Typically, a client should catch this
+ * exception, log it, and stop execution.
  *
  * @author crazybob@google.com (Bob Lee)
  */
@@ -35,9 +39,7 @@
    * Constructs a new exception for the given errors.
    */
   public CreationException(Collection<? extends Message> errorMessages) {
-    super();
-
-    // Sort the messages by source. 
+    // Sort the messages by source.
     this.errorMessages = new ArrayList<Message>(errorMessages);
     Collections.sort(this.errorMessages, new Comparator<Message>() {
       public int compare(Message a, Message b) {
@@ -46,6 +48,10 @@
     });
   }
 
+  public CreationException(Message message) {
+    this.errorMessages = Collections.singletonList(message);
+  }
+
   public String getMessage() {
     return createErrorMessage(errorMessages);
   }
diff --git a/src/com/google/inject/DefaultErrorHandler.java b/src/com/google/inject/DefaultErrorHandler.java
index 36b74dd..7cb118e 100644
--- a/src/com/google/inject/DefaultErrorHandler.java
+++ b/src/com/google/inject/DefaultErrorHandler.java
@@ -18,11 +18,11 @@
 package com.google.inject;
 
 import com.google.inject.internal.ErrorHandler;
-import com.google.inject.internal.ErrorMessages;
+import com.google.inject.internal.ErrorMessage;
 import com.google.inject.spi.Message;
-
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 
 /**
  * A stateful error handler that can be used at both configuration time and
@@ -33,17 +33,17 @@
  * @author jessewilson@google.com (Jesse Wilson)
  */
 class DefaultErrorHandler implements ErrorHandler {
+  private static final Object[] NO_ARGUMENTS = new Object[0];
+
   State state = State.CONFIGURATION_TIME;
   final Collection<Message> errorMessages = new ArrayList<Message>();
 
-  public void handle(Object source, String message) {
-    source = ErrorMessages.convert(source);
-
+  public final void handle(Message message) {
     if (state == State.RUNTIME) {
-      throw new ConfigurationException("Error at " + source + " " + message);
+      throw new CreationException(Collections.singleton(message));
 
     } else if (state == State.CONFIGURATION_TIME) {
-      errorMessages.add(new Message(source, message));
+      errorMessages.add(message);
 
     } else {
       throw new AssertionError();
@@ -53,8 +53,8 @@
   /**
    * Implements formatting. Converts known types to readable strings.
    */
-  public final void handle(Object source, String message, Object... arguments) {
-    handle(source, ErrorMessages.format(message, arguments));
+  public final void handle(Object source, ErrorMessage errorMessage) {
+    handle(new Message(source, errorMessage.toString()));
   }
 
   void blowUpIfErrorsExist() {
diff --git a/src/com/google/inject/ErrorsCommandProcessor.java b/src/com/google/inject/ErrorsCommandProcessor.java
index aa3a6d1..c7d40fb 100644
--- a/src/com/google/inject/ErrorsCommandProcessor.java
+++ b/src/com/google/inject/ErrorsCommandProcessor.java
@@ -19,8 +19,8 @@
 import com.google.inject.commands.AddMessageErrorCommand;
 import com.google.inject.commands.AddThrowableErrorCommand;
 import com.google.inject.internal.ErrorHandler;
-import com.google.inject.internal.ErrorMessages;
-
+import com.google.inject.internal.ErrorMessage;
+import com.google.inject.spi.Message;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -40,17 +40,17 @@
   }
 
   @Override public Boolean visitAddMessageError(AddMessageErrorCommand command) {
-    addError(command.getSource(), command.getMessage(), command.getArguments().toArray());
+    String message = String.format(command.getMessage(), command.getArguments());
+    errorHandler.handle(new Message(command.getSource(), message));
     return true;
   }
 
   @Override public Boolean visitAddError(AddThrowableErrorCommand command) {
     Object source = command.getSource();
-    String message = ErrorMessages.getRootMessage(command.getThrowable());
-    String logMessage = String.format(
-        ErrorMessages.EXCEPTION_REPORTED_BY_MODULE, message);
+    String message = ErrorMessage.getRootMessage(command.getThrowable());
+    String logMessage = ErrorMessage.exceptionReportedByModules(message).toString();
     logger.log(Level.INFO, logMessage, command.getThrowable());
-    addError(source, ErrorMessages.EXCEPTION_REPORTED_BY_MODULE_SEE_LOG, message);
+    addError(source, ErrorMessage.exceptionReportedByModuleSeeLogs(message));
     return true;
   }
 }
diff --git a/src/com/google/inject/FactoryProxy.java b/src/com/google/inject/FactoryProxy.java
index 1e47091..a853b72 100644
--- a/src/com/google/inject/FactoryProxy.java
+++ b/src/com/google/inject/FactoryProxy.java
@@ -45,7 +45,7 @@
         try {
           targetFactory = injector.getInternalFactory(targetKey);
         } catch (ResolveFailedException e) {
-          injector.errorHandler.handle(source, e.getMessage());
+          injector.errorHandler.handle(e.getMessage(source));
         }
       }
     });
diff --git a/src/com/google/inject/GetProviderProcessor.java b/src/com/google/inject/GetProviderProcessor.java
index 2b1a275..78b9978 100644
--- a/src/com/google/inject/GetProviderProcessor.java
+++ b/src/com/google/inject/GetProviderProcessor.java
@@ -37,7 +37,7 @@
     try {
       // ensure the provider can be created
       injector.getProvider(command.getKey());
-    } catch (ConfigurationException e) {
+    } catch (CreationException e) {
       injector.handleMissingBinding(command.getSource(), command.getKey());
     }
 
diff --git a/src/com/google/inject/InjectionPoint.java b/src/com/google/inject/InjectionPoint.java
index 7baad95..4cc472b 100644
--- a/src/com/google/inject/InjectionPoint.java
+++ b/src/com/google/inject/InjectionPoint.java
@@ -16,11 +16,10 @@
 
 package com.google.inject;
 
-import com.google.inject.internal.ErrorMessages;
+import static com.google.common.base.Preconditions.checkNotNull;
+import com.google.inject.internal.ErrorMessage;
 import com.google.inject.internal.ToStringBuilder;
 import com.google.inject.spi.Dependency;
-import static com.google.common.base.Preconditions.checkNotNull;
-
 import java.lang.reflect.Field;
 import java.lang.reflect.Member;
 
@@ -90,12 +89,11 @@
     }
 
     String message = getMember() != null
-        ? String.format(ErrorMessages.CANNOT_INJECT_NULL_INTO_MEMBER, source,
-            getMember())
-        : String.format(ErrorMessages.CANNOT_INJECT_NULL, source);
+        ? ErrorMessage.cannotInjectNullIntoMember(source, getMember()).toString()
+        : ErrorMessage.cannotInjectNull(source).toString();
 
-    throw new ProvisionException(new NullPointerException(message),
-        String.format(ErrorMessages.CANNOT_INJECT_NULL, source));
+    throw new ProvisionException(ErrorMessage.cannotInjectNull(source).toString(),
+        new NullPointerException(message));
   }
 
   // TODO(kevinb): gee, ya think we might want to remove this?
diff --git a/src/com/google/inject/InjectorImpl.java b/src/com/google/inject/InjectorImpl.java
index c4f6b8d..1a68cc7 100644
--- a/src/com/google/inject/InjectorImpl.java
+++ b/src/com/google/inject/InjectorImpl.java
@@ -25,7 +25,7 @@
 import com.google.common.collect.Sets;
 import com.google.inject.internal.Classes;
 import com.google.inject.internal.ErrorHandler;
-import com.google.inject.internal.ErrorMessages;
+import com.google.inject.internal.ErrorMessage;
 import com.google.inject.internal.GuiceFastClass;
 import com.google.inject.internal.Keys;
 import com.google.inject.internal.MatcherAndConverter;
@@ -37,6 +37,7 @@
 import com.google.inject.spi.BindingVisitor;
 import com.google.inject.spi.ConvertedConstantBinding;
 import com.google.inject.spi.Dependency;
+import com.google.inject.spi.Message;
 import com.google.inject.spi.ProviderBinding;
 import com.google.inject.spi.SourceProviders;
 import com.google.inject.util.Providers;
@@ -194,7 +195,7 @@
       try {
         binding = parentInjector.getBinding(key);
       }
-      catch (ConfigurationException e) {
+      catch (CreationException e) {
         // if this happens, the parent can't create this key, and we ignore it
       }
 
@@ -260,7 +261,7 @@
 
     // If the Provider has no type parameter (raw Provider)...
     if (!(providerType instanceof ParameterizedType)) {
-      throw new ResolveFailedException(ErrorMessages.CANNOT_INJECT_RAW_PROVIDER);
+      throw new ResolveFailedException(ErrorMessage.cannotInjectRawProvider());
     }
 
     Type entryType = ((ParameterizedType) providerType).getActualTypeArguments()[0];
@@ -279,10 +280,10 @@
     }
 
     if (otherNames.isEmpty()) {
-      errorHandler.handle(source, ErrorMessages.MISSING_BINDING, key);
+      errorHandler.handle(source, ErrorMessage.missingBinding(key));
     }
     else {
-      errorHandler.handle(source, ErrorMessages.MISSING_BINDING_BUT_OTHERS_EXIST, key, otherNames);
+      errorHandler.handle(source, ErrorMessage.missingBindingButOthersExist(key, otherNames));
     }
   }
 
@@ -358,8 +359,8 @@
     for (MatcherAndConverter<?> converter : converters) {
       if (converter.getTypeMatcher().matches(type)) {
         if (matchingConverter != null) {
-          throw new ResolveFailedException(ErrorMessages.AMBIGUOUS_TYPE_CONVERSION,
-              stringValue, type, matchingConverter, converter);
+          throw new ResolveFailedException(ErrorMessage.ambiguousTypeConversion(
+              stringValue, type, matchingConverter, converter));
         }
         matchingConverter = converter;
       }
@@ -377,13 +378,13 @@
           .convert(stringValue, key.getTypeLiteral());
 
       if (converted == null) {
-        throw new ResolveFailedException(ErrorMessages.CONVERTER_RETURNED_NULL);
+        throw new ResolveFailedException(ErrorMessage.converterReturnedNull());
       }
 
       // We have to filter out primitive types because an Integer is not an instance of int, and we
       // provide converters for all the primitive types and know that they work anyway.
       if (!type.getRawType().isInstance(converted)) {
-        throw new ResolveFailedException(ErrorMessages.CONVERSION_TYPE_ERROR, converted, type);
+        throw new ResolveFailedException(ErrorMessage.conversionTypeError(converted, type));
       }
       return new ConvertedConstantBindingImpl<T>(this, key, converted, stringBinding);
     }
@@ -391,8 +392,8 @@
       throw e;
     }
     catch (Exception e) {
-      throw new ResolveFailedException(ErrorMessages.CONVERSION_ERROR, stringValue,
-          stringBinding.getSource(), type, matchingConverter, e.getMessage());
+      throw new ResolveFailedException(ErrorMessage.conversionError(stringValue,
+          stringBinding.getSource(), type, matchingConverter, e.getMessage()));
     }
   }
 
@@ -473,7 +474,7 @@
       throws ResolveFailedException {
     // Don't try to inject arrays, or enums.
     if (type.isArray() || type.isEnum()) {
-      throw new ResolveFailedException(ErrorMessages.MISSING_BINDING, type);
+      throw new ResolveFailedException(ErrorMessage.missingBinding(type));
     }
 
     // Handle @ImplementedBy
@@ -494,12 +495,12 @@
     // TODO: Method interceptors could actually enable us to implement
     // abstract types. Should we remove this restriction?
     if (Modifier.isAbstract(type.getModifiers())) {
-      throw new ResolveFailedException(ErrorMessages.CANNOT_INJECT_ABSTRACT_TYPE, type);
+      throw new ResolveFailedException(ErrorMessage.cannotInjectAbstractType(type));
     }
 
     // Error: Inner class.
     if (Classes.isInnerClass(type)) {
-      throw new ResolveFailedException(ErrorMessages.CANNOT_INJECT_INNER_CLASS, type);
+      throw new ResolveFailedException(ErrorMessage.cannotInjectInnerClass(type));
     }
 
     if (scope == null) {
@@ -539,7 +540,7 @@
 
     // Make sure it's not the same type. TODO: Can we check for deeper loops?
     if (providerType == type) {
-      throw new ResolveFailedException(ErrorMessages.RECURSIVE_PROVIDER_TYPE);
+      throw new ResolveFailedException(ErrorMessage.recursiveProviderType());
     }
 
     // TODO: Make sure the provided type extends type. We at least check the type at runtime below.
@@ -554,8 +555,8 @@
         Provider<?> provider = providerBinding.internalFactory.get(context, injectionPoint);
         Object o = provider.get();
         if (o != null && !type.isInstance(o)) {
-          errorHandler.handle(StackTraceElements.forType(type), ErrorMessages.SUBTYPE_NOT_PROVIDED,
-              providerType, type);
+          errorHandler.handle(StackTraceElements.forType(type),
+              ErrorMessage.subtypeNotProvided(providerType, type));
           throw new AssertionError();
         }
 
@@ -584,12 +585,12 @@
 
     // Make sure it's not the same type. TODO: Can we check for deeper cycles?
     if (implementationType == type) {
-      throw new ResolveFailedException(ErrorMessages.RECURSIVE_IMPLEMENTATION_TYPE);
+      throw new ResolveFailedException(ErrorMessage.recursiveImplementationType());
     }
 
     // Make sure implementationType extends type.
     if (!type.isAssignableFrom(implementationType)) {
-      throw new ResolveFailedException(ErrorMessages.NOT_A_SUBTYPE, implementationType, type);
+      throw new ResolveFailedException(ErrorMessage.notASubtype(implementationType, type));
     }
 
     // After the preceding check, this cast is safe.
@@ -642,7 +643,7 @@
           // throw with a more appropriate message below
         }
       }
-      throw new ResolveFailedException(ErrorMessages.MISSING_BINDING, key);
+      throw new ResolveFailedException(ErrorMessage.missingBinding(key));
     }
 
     // Create a binding based on the raw type.
@@ -717,7 +718,7 @@
           catch (ResolveFailedException e) {
             if (!inject.optional()) {
               // TODO: Report errors for more than one parameter per member.
-              errorHandler.handle(member, e.getMessage());
+              errorHandler.handle(e.getMessage(member));
             }
           }
         }
@@ -793,7 +794,7 @@
       catch (IllegalAccessException e) {
         throw new AssertionError(e);
       }
-      catch (ConfigurationException e) {
+      catch (CreationException e) {
         throw e;
       }
       catch (ProvisionException provisionException) {
@@ -801,7 +802,8 @@
         throw provisionException;
       }
       catch (RuntimeException runtimeException) {
-        throw new ProvisionException(runtimeException, ErrorMessages.ERROR_INJECTING_FIELD);
+        throw new ProvisionException(
+            ErrorMessage.errorInjectingField().toString(), runtimeException);
       }
       finally {
         context.setInjectionPoint(null);
@@ -884,7 +886,7 @@
       }
       catch (InvocationTargetException e) {
         Throwable cause = e.getCause() != null ? e.getCause() : e;
-        throw new ProvisionException(cause, ErrorMessages.ERROR_INJECTING_METHOD);
+        throw new ProvisionException(ErrorMessage.errorInjectingMethod().toString(), cause);
       }
     }
 
@@ -906,13 +908,14 @@
   final Map<Class<?>, Object> constructors = new ReferenceCache<Class<?>, Object>() {
     @SuppressWarnings("unchecked")
     protected Object create(Class<?> implementation) {
+      // TODO: reduce duplication between this and createUnitializedBinding
       if (!Classes.isConcrete(implementation)) {
         return new ResolveFailedException(
-            ErrorMessages.CANNOT_INJECT_ABSTRACT_TYPE, implementation);
+            ErrorMessage.cannotInjectAbstractType(implementation));
       }
       if (Classes.isInnerClass(implementation)) {
         return new ResolveFailedException(
-            ErrorMessages.CANNOT_INJECT_INNER_CLASS, implementation);
+            ErrorMessage.cannotInjectInnerClass(implementation));
       }
       return new ConstructorInjector(InjectorImpl.this, implementation);
     }
@@ -933,7 +936,7 @@
       try {
         return factory.get(context, injectionPoint);
       }
-      catch (ConfigurationException e) {
+      catch (CreationException e) {
         throw e;
       }
       catch (ProvisionException provisionException) {
@@ -941,7 +944,8 @@
         throw provisionException;
       }
       catch (RuntimeException runtimeException) {
-        throw new ProvisionException(runtimeException, ErrorMessages.ERROR_INJECTING_METHOD);
+        throw new ProvisionException(
+            ErrorMessage.errorInjectingMethod().toString(), runtimeException);
       }
       finally {
         context.setInjectionPoint(null);
@@ -1021,8 +1025,8 @@
       return getProviderOrThrow(key);
     }
     catch (ResolveFailedException e) {
-      throw new ConfigurationException(
-          "Missing binding to " + ErrorMessages.convert(key) + ": " + e.getMessage());
+      throw new CreationException(new Message(
+          ErrorMessage.bindingNotFound(key, e.getMessage()).toString()));
     }
   }
 
diff --git a/src/com/google/inject/InternalFactoryToProviderAdapter.java b/src/com/google/inject/InternalFactoryToProviderAdapter.java
index 6a21b40..5dfdfc8 100644
--- a/src/com/google/inject/InternalFactoryToProviderAdapter.java
+++ b/src/com/google/inject/InternalFactoryToProviderAdapter.java
@@ -16,9 +16,9 @@
 
 package com.google.inject;
 
-import com.google.inject.internal.ErrorMessages;
-import com.google.inject.spi.SourceProviders;
 import static com.google.common.base.Preconditions.checkNotNull;
+import com.google.inject.internal.ErrorMessage;
+import com.google.inject.spi.SourceProviders;
 
 /**
  * @author crazybob@google.com (Bob Lee)
@@ -45,7 +45,7 @@
     } catch(ProvisionException e) {
       throw e;
     } catch(RuntimeException e) {
-      throw new ProvisionException(e, ErrorMessages.ERROR_IN_PROVIDER);
+      throw new ProvisionException(ErrorMessage.errorInProvider().toString(), e);
     }
   }
 
diff --git a/src/com/google/inject/InvalidErrorHandler.java b/src/com/google/inject/InvalidErrorHandler.java
deleted file mode 100644
index 81af073..0000000
--- a/src/com/google/inject/InvalidErrorHandler.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * Copyright (C) 2008 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package com.google.inject;
-
-import com.google.inject.internal.ErrorHandler;
-
-/**
- * @author crazybob@google.com (Bob Lee)
- */
-class InvalidErrorHandler implements ErrorHandler {
-  public void handle(Object source, String message) {
-    throw new AssertionError(message);
-  }
-
-  public void handle(Object source, String message, Object... arguments) {
-    throw new AssertionError(message);
-  }
-}
diff --git a/src/com/google/inject/ProviderMethods.java b/src/com/google/inject/ProviderMethods.java
index a6d021b..ba46ab9 100644
--- a/src/com/google/inject/ProviderMethods.java
+++ b/src/com/google/inject/ProviderMethods.java
@@ -16,11 +16,10 @@
 
 package com.google.inject;
 
-import com.google.inject.internal.ErrorMessages;
+import com.google.inject.internal.ErrorMessage;
 import com.google.inject.internal.StackTraceElements;
 import com.google.inject.spi.SourceProvider;
 import com.google.inject.spi.SourceProviders;
-
 import java.lang.annotation.Annotation;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
@@ -160,10 +159,8 @@
         if (annotation.annotationType()
             .isAnnotationPresent(ScopeAnnotation.class)) {
           if (found != null) {
-            addError(ErrorMessages.DUPLICATE_SCOPE_ANNOTATIONS,
-                "@" + found.getSimpleName(),
-                "@" + annotation.annotationType().getSimpleName()
-            );
+            addError(ErrorMessage.duplicateScopeAnnotations(
+                found, annotation.annotationType()).toString());
           } else {
             found = annotation.annotationType();
           }
@@ -180,10 +177,8 @@
         if (annotation.annotationType()
             .isAnnotationPresent(BindingAnnotation.class)) {
           if (found != null) {
-            addError(ErrorMessages.DUPLICATE_BINDING_ANNOTATIONS,
-                "@" + found.annotationType().getSimpleName(),
-                "@" + annotation.annotationType().getSimpleName()
-            );
+            addError(ErrorMessage.duplicateBindingAnnotations(
+                found.annotationType(), annotation.annotationType()).toString());
           } else {
             found = annotation;
           }
diff --git a/src/com/google/inject/ProvisionException.java b/src/com/google/inject/ProvisionException.java
index 07658e6..d122419 100644
--- a/src/com/google/inject/ProvisionException.java
+++ b/src/com/google/inject/ProvisionException.java
@@ -16,10 +16,10 @@
 
 package com.google.inject;
 
-import com.google.inject.internal.ErrorMessages;
-import static com.google.inject.internal.ErrorMessages.*;
+import static com.google.inject.internal.ErrorMessage.whileLocatingField;
+import static com.google.inject.internal.ErrorMessage.whileLocatingParameter;
+import static com.google.inject.internal.ErrorMessage.whileLocatingValue;
 import com.google.inject.internal.StackTraceElements;
-
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.lang.reflect.Member;
@@ -35,17 +35,15 @@
  */
 public class ProvisionException extends RuntimeException {
 
-  private final String errorMessage;
   private final List<String> contexts = new ArrayList<String>(5);
 
-  public ProvisionException(Throwable cause, String errorMessage) {
-    super(errorMessage, cause);
-    this.errorMessage = errorMessage;
+  public ProvisionException(String message, Throwable cause) {
+    super(message, cause);
   }
 
   @Override public String getMessage() {
     StringBuilder result = new StringBuilder();
-    result.append(errorMessage);
+    result.append(super.getMessage());
 
     for (int i = contexts.size() - 1; i >= 0; i--) {
       result.append(String.format("%n"));
@@ -69,20 +67,19 @@
    */
   private String contextToSnippet(InjectionPoint injectionPoint) {
     Key<?> key = injectionPoint.getKey();
-    Object keyDescription = ErrorMessages.convert(key);
     Member member = injectionPoint.getMember();
 
     if (member instanceof Field) {
-      return String.format(ERROR_WHILE_LOCATING_FIELD,
-          keyDescription, StackTraceElements.forMember(member));
+      return whileLocatingField(
+          key, StackTraceElements.forMember(member)).toString();
 
     } else if (member instanceof Method || member instanceof Constructor) {
-      return String.format(ERROR_WHILE_LOCATING_PARAMETER,
-          keyDescription, injectionPoint.getParameterIndex(),
-          StackTraceElements.forMember(member));
+      return whileLocatingParameter(
+          key, injectionPoint.getParameterIndex(),
+          StackTraceElements.forMember(member)).toString();
 
     } else {
-      return String.format(ERROR_WHILE_LOCATING_VALUE, keyDescription);
+      return whileLocatingValue(key).toString();
     }
   }
 }
diff --git a/src/com/google/inject/RuntimeReflectionFactory.java b/src/com/google/inject/RuntimeReflectionFactory.java
index f7ba25e..1c6f959 100644
--- a/src/com/google/inject/RuntimeReflectionFactory.java
+++ b/src/com/google/inject/RuntimeReflectionFactory.java
@@ -17,9 +17,10 @@
 
 package com.google.inject;
 
-import com.google.inject.internal.*;
 import static com.google.common.base.Preconditions.checkNotNull;
-
+import com.google.inject.internal.ErrorHandler;
+import com.google.inject.internal.ErrorMessage;
+import com.google.inject.internal.StackTraceElements;
 import java.lang.reflect.Constructor;
 
 /**
@@ -56,13 +57,13 @@
           if (inject.optional()) {
             errorHandler.handle(
                 StackTraceElements.forMember(constructor),
-                ErrorMessages.OPTIONAL_CONSTRUCTOR);
+                ErrorMessage.optionalConstructor());
           }
 
           if (found != null) {
             errorHandler.handle(
                 StackTraceElements.forMember(found),
-                ErrorMessages.TOO_MANY_CONSTRUCTORS);
+                ErrorMessage.tooManyConstructors());
             return invalidConstructor();
           }
           found = constructor;
@@ -81,8 +82,7 @@
         errorHandler.handle(
             StackTraceElements.forMember(
                 implementation.getDeclaredConstructors()[0]),
-            ErrorMessages.MISSING_CONSTRUCTOR,
-            implementation);
+            ErrorMessage.missingConstructor(implementation));
         return invalidConstructor();
       }
     }
diff --git a/src/com/google/inject/Scopes.java b/src/com/google/inject/Scopes.java
index bb1eff4..552773a 100644
--- a/src/com/google/inject/Scopes.java
+++ b/src/com/google/inject/Scopes.java
@@ -17,9 +17,8 @@
 package com.google.inject;
 
 import com.google.inject.internal.ErrorHandler;
-import com.google.inject.internal.ErrorMessages;
+import com.google.inject.internal.ErrorMessage;
 import com.google.inject.internal.StackTraceElements;
-
 import java.lang.annotation.Annotation;
 import java.util.Map;
 
@@ -105,13 +104,10 @@
     for (Annotation annotation : implementation.getAnnotations()) {
       if (isScopeAnnotation(annotation)) {
         if (found != null) {
-          errorHandler.handle(
-              StackTraceElements.forType(implementation),
-              ErrorMessages.DUPLICATE_SCOPE_ANNOTATIONS,
-              "@" + found.getSimpleName(),
-              "@" + annotation.annotationType().getSimpleName()
-          );
-        } else {
+          errorHandler.handle(StackTraceElements.forType(implementation),
+              ErrorMessage.duplicateScopeAnnotations(found, annotation.annotationType()));
+        }
+        else {
           found = annotation.annotationType();
         }
       }
diff --git a/src/com/google/inject/ScopesCommandProcessor.java b/src/com/google/inject/ScopesCommandProcessor.java
index 87c0ce7..c2a2894 100644
--- a/src/com/google/inject/ScopesCommandProcessor.java
+++ b/src/com/google/inject/ScopesCommandProcessor.java
@@ -16,13 +16,12 @@
 
 package com.google.inject;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import com.google.inject.commands.BindScopeCommand;
 import com.google.inject.internal.Annotations;
 import com.google.inject.internal.ErrorHandler;
-import com.google.inject.internal.ErrorMessages;
+import com.google.inject.internal.ErrorMessage;
 import com.google.inject.internal.StackTraceElements;
-import static com.google.common.base.Preconditions.checkNotNull;
-
 import java.lang.annotation.Annotation;
 import java.util.Map;
 
@@ -48,20 +47,19 @@
 
     if (!Scopes.isScopeAnnotation(annotationType)) {
       addError(StackTraceElements.forType(annotationType),
-          ErrorMessages.MISSING_SCOPE_ANNOTATION);
+          ErrorMessage.missingScopeAnnotation());
       // Go ahead and bind anyway so we don't get collateral errors.
     }
 
     if (!Annotations.isRetainedAtRuntime(annotationType)) {
       addError(StackTraceElements.forType(annotationType),
-          ErrorMessages.MISSING_RUNTIME_RETENTION, command.getSource());
+          ErrorMessage.missingRuntimeRetention(command.getSource()));
       // Go ahead and bind anyway so we don't get collateral errors.
     }
 
     Scope existing = scopes.get(checkNotNull(annotationType, "annotation type"));
     if (existing != null) {
-      addError(command.getSource(), ErrorMessages.DUPLICATE_SCOPES, existing,
-          annotationType, scope);
+      addError(command.getSource(), ErrorMessage.duplicateScopes(existing, annotationType, scope));
     } else {
       scopes.put(annotationType, checkNotNull(scope, "scope"));
     }
diff --git a/src/com/google/inject/commands/BindCommand.java b/src/com/google/inject/commands/BindCommand.java
index e012873..9adb025 100644
--- a/src/com/google/inject/commands/BindCommand.java
+++ b/src/com/google/inject/commands/BindCommand.java
@@ -16,15 +16,18 @@
 
 package com.google.inject.commands;
 
-import com.google.inject.*;
+import static com.google.common.base.Preconditions.checkNotNull;
+import com.google.inject.Binder;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Scope;
+import com.google.inject.TypeLiteral;
 import com.google.inject.binder.AnnotatedBindingBuilder;
 import com.google.inject.binder.ConstantBindingBuilder;
 import com.google.inject.binder.LinkedBindingBuilder;
 import com.google.inject.binder.ScopedBindingBuilder;
-import com.google.inject.internal.ErrorMessages;
+import com.google.inject.internal.ErrorMessage;
 import com.google.inject.spi.SourceProviders;
-import static com.google.common.base.Preconditions.checkNotNull;
-
 import java.lang.annotation.Annotation;
 
 /**
@@ -183,7 +186,7 @@
     }
 
     public void toInstance(final T instance) {
-      checkNotNull(instance, ErrorMessages.CANNOT_BIND_TO_NULL_INSTANCE);
+      checkNotNull(instance, ErrorMessage.cannotBindToNullInstance().toString());
 
       checkNotTargetted();
       bindTarget = new AbstractTarget<T>() {
diff --git a/src/com/google/inject/internal/ErrorHandler.java b/src/com/google/inject/internal/ErrorHandler.java
index 60ad31e..2017d69 100644
--- a/src/com/google/inject/internal/ErrorHandler.java
+++ b/src/com/google/inject/internal/ErrorHandler.java
@@ -16,6 +16,8 @@
 
 package com.google.inject.internal;
 
+import com.google.inject.spi.Message;
+
 /**
  * Handles errors in the Injector.
  *
@@ -26,10 +28,10 @@
   /**
    * Handles an error.
    */
-  void handle(Object source, String message);
+  void handle(Object source, ErrorMessage errorMessage);
 
   /**
-   * Handles an error.
+   * Handles a user-reported error.
    */
-  void handle(Object source, String message, Object... arguments);
+  void handle(Message message);
 }
diff --git a/src/com/google/inject/internal/ErrorMessage.java b/src/com/google/inject/internal/ErrorMessage.java
new file mode 100644
index 0000000..af86717
--- /dev/null
+++ b/src/com/google/inject/internal/ErrorMessage.java
@@ -0,0 +1,327 @@
+/**
+ * Copyright (C) 2006 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.inject.internal;
+
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Scope;
+import com.google.inject.TypeLiteral;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * An error message.
+ *
+ * @author crazybob@google.com (Bob Lee)
+ */
+public final class ErrorMessage {
+
+  static final Collection<Converter<?>> converters = createConverters();
+
+  public static Object convert(Object o) {
+    for (Converter<?> converter : converters) {
+      if (converter.appliesTo(o)) {
+        return converter.convert(o);
+      }
+    }
+    return o;
+  }
+
+  @SuppressWarnings("unchecked") // for generic array creation.
+  static Collection<Converter<?>> createConverters() {
+    return Arrays.asList(new Converter<MatcherAndConverter>(MatcherAndConverter.class) {
+      public String toString(MatcherAndConverter m) {
+        return m.toString();
+      }
+    }, new Converter<Method>(Method.class) {
+      public String toString(Method m) {
+        return "method " + m.getDeclaringClass().getName() + "." + m.getName() + "()";
+      }
+    }, new Converter<Constructor>(Constructor.class) {
+      public String toString(Constructor c) {
+        return "constructor " + c.getDeclaringClass().getName() + "()";
+      }
+    }, new Converter<Field>(Field.class) {
+      public String toString(Field f) {
+        return "field " + f.getDeclaringClass().getName() + "." + f.getName();
+      }
+    }, new Converter<Class>(Class.class) {
+      public String toString(Class c) {
+        return c.getName();
+      }
+    }, new Converter<Key>(Key.class) {
+      public String toString(Key k) {
+        StringBuilder result = new StringBuilder();
+        result.append(k.getTypeLiteral());
+        if (k.getAnnotationType() != null) {
+          result.append(" annotated with ");
+          result.append(k.getAnnotation() != null ? k.getAnnotation() : k.getAnnotationType());
+        }
+        return result.toString();
+      }
+    });
+  }
+
+  public static ErrorMessage missingBinding(Object keyOrType) {
+    return new ErrorMessage("Binding to %s not found. No bindings to that type were found.",
+        keyOrType);
+  }
+
+  public static ErrorMessage missingBindingButOthersExist(Key<?> key, List<String> otherNames) {
+    return new ErrorMessage(
+        "Binding to %s not found. Annotations on other bindings to that type include: %s", key,
+        otherNames);
+  }
+
+  public static ErrorMessage converterReturnedNull() {
+    return new ErrorMessage("Converter returned null.");
+  }
+
+  public static ErrorMessage conversionTypeError(Object converted, TypeLiteral<?> type) {
+    return new ErrorMessage("Converter returned %s but we expected a[n] %s.", converted, type);
+  }
+
+  public static ErrorMessage conversionError(String stringValue, Object source,
+      TypeLiteral<?> type, MatcherAndConverter<?> matchingConverter, String message) {
+    return new ErrorMessage("Error converting '%s' (bound at %s) to %s using %s. Reason: %s",
+        stringValue, source, type, matchingConverter, message);
+  }
+
+  public static ErrorMessage ambiguousTypeConversion(String stringValue, TypeLiteral<?> type,
+      MatcherAndConverter<?> matchingConverter, MatcherAndConverter<?> converter) {
+    return new ErrorMessage("Error converting '%s' to  %s. "
+        + "More than one type converter can apply: %s, and %s. "
+        + "Please adjust your type converter configuration to avoid  overlapping matches.",
+        stringValue, type, matchingConverter, converter);
+  }
+
+  public static ErrorMessage bindingNotFound(Key<?> key, String message) {
+    return new ErrorMessage("Binding to %s not found: %s", key, message);
+  }
+
+  public static ErrorMessage bindingToProvider() {
+    return new ErrorMessage("Binding to Provider is not allowed.");
+  }
+
+  public static ErrorMessage subtypeNotProvided(Class<? extends Provider<?>> providerType,
+      Class<?> type) {
+    return new ErrorMessage("%s doesn't provide instances of %s.", providerType, type);
+  }
+
+  public static ErrorMessage notASubtype(Class<?> implementationType, Class<?> type) {
+    return new ErrorMessage("%s doesn't extend %s.", implementationType, type);
+  }
+
+  public static ErrorMessage recursiveImplementationType() {
+    return new ErrorMessage("@ImplementedBy points to the same class it annotates.");
+  }
+
+  public static ErrorMessage recursiveProviderType() {
+    return new ErrorMessage("@ProvidedBy points to the same class it annotates.");
+  }
+
+  public static ErrorMessage exceptionReportedByModules(String message) {
+    return new ErrorMessage("An exception was caught and reported. Message: %s", message);
+  }
+
+  public static ErrorMessage exceptionReportedByModuleSeeLogs(String message) {
+    return new ErrorMessage("An exception was caught and reported. "
+        + "See log for details. Message: %s", message);
+  }
+
+  public static ErrorMessage missingImplementation() {
+    return new ErrorMessage("No implementation was specified.");
+  }
+
+  public static ErrorMessage missingBindingAnnotation(Object source) {
+    return new ErrorMessage("Please annotate with @BindingAnnotation. Bound at %s.", source);
+  }
+
+  public static ErrorMessage missingRuntimeRetention(Object source) {
+    return new ErrorMessage("Please annotate with @Retention(RUNTIME). Bound at %s.", source);
+  }
+
+  public static ErrorMessage missingScopeAnnotation() {
+    return new ErrorMessage("Please annotate with @ScopeAnnotation.");
+  }
+
+  public static ErrorMessage optionalConstructor() {
+    return new ErrorMessage("@Inject(optional=true) is not allowed on constructors.");
+  }
+
+//  public static ErrorMessage constantConversionError() {
+//    return new ErrorMessage("Error converting String constant bound at %s to %s: %s");
+//  }
+//
+  public static ErrorMessage cannotBindToGuiceType(String simpleName) {
+    return new ErrorMessage("Binding to core guice framework type is not allowed: %s.", simpleName);
+  }
+
+  public static ErrorMessage cannotBindToNullInstance() {
+    return new ErrorMessage("Binding to null instances is not allowed. "
+        + "Use toProvider(Providers.of(null)) if this is your intended behaviour.");
+  }
+
+  public static ErrorMessage scopeNotFound(String s) {
+    return new ErrorMessage("No scope is bound to %s.", s);
+  }
+
+  private static final String CONSTRUCTOR_RULES =
+      "Classes must have either one (and only one) constructor "
+          + "annotated with @Inject or a zero-argument constructor.";
+
+  public static ErrorMessage missingConstructor(Class<?> implementation) {
+    return new ErrorMessage("Could not find a suitable constructor in %s. " + CONSTRUCTOR_RULES,
+        implementation);
+  }
+
+  public static ErrorMessage tooManyConstructors() {
+    return new ErrorMessage(
+        "Found more than one constructor annotated with @Inject. " + CONSTRUCTOR_RULES);
+  }
+
+  public static ErrorMessage duplicateScopes(Scope existing,
+      Class<? extends Annotation> annotationType, Scope scope) {
+    return new ErrorMessage("Scope %s is already bound to %s. Cannot bind %s.", existing,
+        annotationType, scope);
+  }
+
+  public static ErrorMessage missingConstantValues() {
+    return new ErrorMessage("Missing constant value. Please call to(...).");
+  }
+
+  public static ErrorMessage cannotInjectAbstractType(Class<?> type) {
+    return new ErrorMessage("Injecting into abstract types is not supported. "
+        + "Please use a concrete type instead of %s.", type);
+  }
+
+  public static ErrorMessage cannotInjectInnerClass(Class<?> type) {
+    return new ErrorMessage("Injecting into inner classes is not supported.  "
+        + "Please use a 'static' class (top-level or nested) instead of %s.", type);
+  }
+
+  public static ErrorMessage duplicateBindingAnnotations(
+      Class<? extends Annotation> a, Class<? extends Annotation> b) {
+    return new ErrorMessage(
+        "Found more than one annotation annotated with @BindingAnnotation: %s and %s", a, b);
+  }
+
+  public static ErrorMessage duplicateScopeAnnotations(
+      Class<? extends Annotation> a, Class<? extends Annotation> b) {
+    return new ErrorMessage("More than one scope annotation was found: %s and %s", a, b);
+  }
+
+  public static ErrorMessage recursiveBinding() {
+    return new ErrorMessage("Binding points to itself.");
+  }
+
+  public static ErrorMessage bindingAlreadySet(Key<?> key, Object source) {
+    return new ErrorMessage("A binding to %s was already configured at %s.", key, source);
+  }
+
+  public static ErrorMessage errorInjectingField() {
+    return new ErrorMessage("Error injecting field");
+  }
+
+  public static ErrorMessage errorInjectingMethod() {
+    return new ErrorMessage("Error injecting method");
+  }
+
+  public static ErrorMessage errorInjectingConstructor() {
+    return new ErrorMessage("Error injecting constructor");
+  }
+
+  public static ErrorMessage errorInProvider() {
+    return new ErrorMessage("Error in custom provider");
+  }
+
+  public static ErrorMessage whileLocatingField(Key key, Object source) {
+    return new ErrorMessage("  while locating %s%n    for field at %s", key, source);
+  }
+
+  public static ErrorMessage whileLocatingParameter(Key key,
+      int parameterIndex, Object source) {
+    return new ErrorMessage("  while locating %s%n    for parameter %s at %s",
+        key, parameterIndex, source);
+  }
+
+  public static ErrorMessage whileLocatingValue(Key key) {
+    return new ErrorMessage("  while locating %s", key);
+  }
+
+  public static ErrorMessage cannotInjectNull(Object source) {
+    return new ErrorMessage("null returned by binding at %s", source);
+  }
+
+  public static ErrorMessage cannotInjectNullIntoMember(Object source, Member member) {
+    return new ErrorMessage("null returned by binding at %s%n but %s is not @Nullable",
+        source, member);
+  }
+
+  public static ErrorMessage cannotInjectRawProvider() {
+    return new ErrorMessage("Cannot inject a Provider that has no type parameter");
+  }
+
+  public static ErrorMessage cannotSatisfyCircularDependency(Class<?> expectedType) {
+    return new ErrorMessage(
+        "Tried proxying %s to support a circular dependency, but it is not an interface.",
+        expectedType);
+  }
+
+  private final String formatted;
+
+  private ErrorMessage(String messageFormat, Object... arguments) {
+    for (int i = 0; i < arguments.length; i++) {
+      arguments[i] = ErrorMessage.convert(arguments[i]);
+    }
+    this.formatted = String.format(messageFormat, arguments);
+  }
+
+  @Override public String toString() {
+    return formatted;
+  }
+
+  static abstract class Converter<T> {
+
+    final Class<T> type;
+
+    Converter(Class<T> type) {
+      this.type = type;
+    }
+
+    boolean appliesTo(Object o) {
+      return type.isAssignableFrom(o.getClass());
+    }
+
+    String convert(Object o) {
+      return toString(type.cast(o));
+    }
+
+    abstract String toString(T t);
+  }
+
+  public static String getRootMessage(Throwable t) {
+    Throwable cause = t.getCause();
+    return cause == null ? t.toString() : getRootMessage(cause);
+  }
+}
diff --git a/src/com/google/inject/internal/Keys.java b/src/com/google/inject/internal/Keys.java
index 0444981..7001acf 100644
--- a/src/com/google/inject/internal/Keys.java
+++ b/src/com/google/inject/internal/Keys.java
@@ -17,12 +17,11 @@
 
 package com.google.inject.internal;
 
-import com.google.inject.Key;
 import com.google.inject.BindingAnnotation;
-
-import java.lang.reflect.Type;
-import java.lang.reflect.Member;
+import com.google.inject.Key;
 import java.lang.annotation.Annotation;
+import java.lang.reflect.Member;
+import java.lang.reflect.Type;
 
 public class Keys {
 
@@ -38,7 +37,8 @@
           found = annotation;
         } else {
           errorHandler.handle(StackTraceElements.forMember(member),
-              ErrorMessages.DUPLICATE_BINDING_ANNOTATIONS, found, annotation);
+              ErrorMessage.duplicateBindingAnnotations(
+                  found.annotationType(), annotation.annotationType()));
         }
       }
     }
diff --git a/src/com/google/inject/internal/ResolveFailedException.java b/src/com/google/inject/internal/ResolveFailedException.java
index 8e7663c..1729487 100644
--- a/src/com/google/inject/internal/ResolveFailedException.java
+++ b/src/com/google/inject/internal/ResolveFailedException.java
@@ -17,6 +17,8 @@
 
 package com.google.inject.internal;
 
+import com.google.inject.spi.Message;
+
 /**
  * Indicates that resolving a binding failed. This is thrown when resolving a
  * new binding, either at injector-creation time or when resolving a
@@ -26,7 +28,11 @@
  */
 public class ResolveFailedException extends Exception {
 
-  public ResolveFailedException(String message, Object... arguments) {
-    super(ErrorMessages.format(message, arguments));
+  public ResolveFailedException(ErrorMessage errorMessage) {
+    super(errorMessage.toString());
+  }
+
+  public Message getMessage(Object source) {
+    return new Message(source, getMessage());
   }
 }