Aha! Finally found a reliable way to detect jsonp loading errors.

Before, afterquery would just stop working if your jsonp data source didn't
load for any reason (eg. a syntax error in the input file, like if it's not
really jsonp).  Now we load a *second* <script> file after the first one;
that file always runs, but only after the first one has finished for any
reason (successfully or unsuccessfully).  Since when it succeeds, it calls
successfunc, we need only check whether successfunc has been called to see
if we have a failure.  No stupid timeouts!
diff --git a/postscript.js b/postscript.js
new file mode 100644
index 0000000..732e782
--- /dev/null
+++ b/postscript.js
@@ -0,0 +1 @@
+onpostscript();
diff --git a/render.html b/render.html
index e27a804..1ff0946 100644
--- a/render.html
+++ b/render.html
@@ -74,10 +74,13 @@
 <body>
 
 <div id='editmenu'><a id='editlink' href='' target='_top'>edit</a></div>
-<div id='vizstatus'><div id='statustext'></div><div id='statussub'></div></div>
+<div id='vizstatus'>
+  <div id='statustext'></div>
+  <div id='statussub'></div>
+  <div id='vizlog'></div>
+</div>
 <div id='vizraw'></div>
 <div id='vizchart'></div>
-<div id='vizlog'></div>
 
 <script>
 $('#editlink').attr('href', '/edit' + window.location.search);
diff --git a/render.js b/render.js
index f748c0e..37ed994 100644
--- a/render.js
+++ b/render.js
@@ -1640,13 +1640,17 @@
 
   function getUrlData(url, success_func, error_func) {
     console.debug('fetching data url:', url);
+    var successfunc_called;
 
     var iframe = document.createElement('iframe');
     iframe.style.display = 'none';
 
     iframe.onload = function() {
       // the default jsonp callback
-      iframe.contentWindow.jsonp = success_func;
+      iframe.contentWindow.jsonp = function(data) {
+        success_func(data);
+        successfunc_called = true;
+      }
 
       // a callback the jsonp can execute if oauth2 authentication is needed
       iframe.contentWindow.tryOAuth2 = function(oauth2_url) {
@@ -1699,9 +1703,18 @@
       };
 
       iframe.contentWindow.onerror = function(message, xurl, lineno) {
-        err(null, message + ' url=' + xurl + ' line=' + lineno);
+        err(message + ' url=' + xurl + ' line=' + lineno);
       };
 
+      iframe.contentWindow.onpostscript = function() {
+        if (successfunc_called) {
+          console.debug('json load was successful.');
+        } else {
+          err('Error loading data; check javascript console for details.');
+          err('<a href="' + encodeURI(url) + '">' + encodeURI(url) + '</a>');
+        }
+      }
+
       iframe.contentWindow.jsonp_url = url;
 
       //TODO(apenwarr): change the domain/origin attribute of the iframe.
@@ -1710,10 +1723,17 @@
       //  localStorage, set cookies, etc.  We can use the new html5 postMessage
       //  feature to safely send json data from the iframe back to us.
       // ...but for the moment we have to trust the data provider.
+      //TODO(apenwarr): at that time, make this script.async=1 and run postscript from there.
+
       var script = iframe.contentDocument.createElement('script');
-      script.async = 1;
+      script.async = false;
       script.src = url;
       iframe.contentDocument.body.appendChild(script);
+
+      var postscript = iframe.contentDocument.createElement('script');
+      postscript.async = false;
+      postscript.src = 'postscript.js';
+      iframe.contentDocument.body.appendChild(postscript);
     };
     document.body.appendChild(iframe);
   }