diff --git a/src/state.js b/src/state.js
index 92ec891e9..b4c26ffdf 100644
--- a/src/state.js
+++ b/src/state.js
@@ -184,12 +184,7 @@ function $StateProvider(   $urlRouterProvider,   $urlMatcherFactory) {
       }
 
       // Normalize/filter parameters before we pass them to event handlers etc.
-      var normalizedToParams = {};
-      forEach(to.params, function (name) {
-        var value = toParams[name];
-        normalizedToParams[name] = (value != null) ? String(value) : null;
-      });
-      toParams = normalizedToParams;
+      toParams = normalize(to.params, toParams || {});
 
       // Broadcast start event and cancel the transition if requested
       if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams)
@@ -271,6 +266,11 @@ function $StateProvider(   $urlRouterProvider,   $urlMatcherFactory) {
       return $state.$current.includes[findState(stateOrName).name];
     };
 
+    $state.href = function (stateOrName, params) {
+      var state = findState(stateOrName), nav = state.navigable;
+      if (!nav) throw new Error("State '" + state + "' is not navigable");
+      return nav.url.format(normalize(state.params, params || {}));
+    };
 
     function resolveState(state, params, paramsAreFiltered, inherited, dst) {
       // We need to track all the promises generated during the resolution process.
@@ -345,6 +345,16 @@ function $StateProvider(   $urlRouterProvider,   $urlMatcherFactory) {
       });
     }
 
+    function normalize(keys, values) {
+      var normalized = {};
+
+      forEach(keys, function (name) {
+        var value = values[name];
+        normalized[name] = (value != null) ? String(value) : null;
+      });
+      return normalized;
+    }
+
     function equalForKeys(a, b, keys) {
       for (var i=0; i<keys.length; i++) {
         var k = keys[i];
diff --git a/test/stateSpec.js b/test/stateSpec.js
index 901a299cb..f9398f42c 100644
--- a/test/stateSpec.js
+++ b/test/stateSpec.js
@@ -32,7 +32,13 @@ describe('state', function () {
       .state('C', C)
       .state('D', D)
       .state('DD', DD)
-      .state('E', E);
+      .state('E', E)
+
+      .state('home', { url: "/" })
+      .state('home.item', { url: "front/:id" })
+      .state('about', { url: "/about" })
+      .state('about.person', { url: "/:person" })
+      .state('about.person.item', { url: "/:id" });
 
     $provide.value('AppInjectable', AppInjectable);
   }));
@@ -226,7 +232,7 @@ describe('state', function () {
     }));
   });
 
-  
+
   describe('.transition', function () {
     it('is null when no transition is taking place', inject(function ($state, $q) {
       expect($state.transition).toBeNull();
@@ -240,4 +246,22 @@ describe('state', function () {
       expect($state.transition).toBe(trans);
     }));
   });
+
+
+  describe('.href()', function () {
+    it('aborts on un-navigable states', inject(function ($state) {
+      expect(function() { $state.href("A"); }).toThrow("State 'A' is not navigable");
+    }));
+
+    it('generates a URL without parameters', inject(function ($state) {
+      expect($state.href("home")).toEqual("/");
+      expect($state.href("about", {})).toEqual("/about");
+      expect($state.href("about", { foo: "bar" })).toEqual("/about");
+    }));
+
+    it('generates a URL with parameters', inject(function ($state) {
+      expect($state.href("about.person", { person: "bob" })).toEqual("/about/bob");
+      expect($state.href("about.person.item", { person: "bob", id: null })).toEqual("/about/bob/");
+    }));
+  });
 });