diff --git a/app/locale/en.coffee b/app/locale/en.coffee
index 6e3aebf3e..7d35d42aa 100644
--- a/app/locale/en.coffee
+++ b/app/locale/en.coffee
@@ -12,6 +12,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
     manual: "Manual"
     fork: "Fork"
     play: "Play"
+    retry: "Retry"
 
   units:
     second: "second"
@@ -602,3 +603,27 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
     tutorial: "tutorial"
     new_to_programming: ". New to programming? Hit our beginner campaign to skill up."
     so_ready: "I Am So Ready for This"
+
+  loading_error:
+    could_not_load: "Error loading from server"
+    connection_failure: "Connection failed."
+    unauthorized: "You need to be signed in. Do you have cookies disabled?"
+    forbidden: "You do not have the permissions."
+    not_found: "Not found."
+    not_allowed: "Method not allowed."
+    timeout: "Server timeout."
+    conflict: "Resource conflict."
+    bad_input: "Bad input."
+    server_error: "Server error."
+    unknown: "Unknown error."
+    
+  resources:
+    your_sessions: "Your Sessions"
+    level: "Level"
+    social_network_apis: "Social Network APIs"
+    facebook_status: "Facebook Status"
+    facebook_friends: "Facebook Friends"
+    facebook_friend_sessions: "Facebook Friend Sessions"
+    gplus_friends: "G+ Friends"
+    gplus_friend_sessions: "G+ Friend Sessions"
+    leaderboard: 'leaderboard'
\ No newline at end of file
diff --git a/app/styles/base.sass b/app/styles/base.sass
index fc6e45c03..0e9e2c8ee 100644
--- a/app/styles/base.sass
+++ b/app/styles/base.sass
@@ -167,7 +167,15 @@ a[data-toggle="modal"]
     width: 50%
     margin: 0 25%
   .progress-bar
-    width: 100%
+    width: 0%
+    transition: width 0.1s ease
+    
+  .errors .alert
+    padding: 5px
+    display: block
+    margin: 10px auto
+    .btn
+      margin-left: 10px
   
 .modal
   .wait
diff --git a/app/templates/loading.jade b/app/templates/loading.jade
index 519808a00..df2fc0eb5 100644
--- a/app/templates/loading.jade
+++ b/app/templates/loading.jade
@@ -1,4 +1,6 @@
 .loading-screen
   h1(data-i18n="common.loading") Loading...
-  .progress.progress-striped.active
-    .progress-bar
\ No newline at end of file
+  .progress
+    .progress-bar
+      
+  .errors
\ No newline at end of file
diff --git a/app/templates/loading_error.jade b/app/templates/loading_error.jade
new file mode 100644
index 000000000..b56ede4ac
--- /dev/null
+++ b/app/templates/loading_error.jade
@@ -0,0 +1,31 @@
+.alert.alert-danger.loading-error-alert
+  span(data-i18n="loading_error.could_not_load") Error loading from server
+  span  (
+  span(data-i18n="resources.#{name}")
+  span ) 
+  if !responseText
+    strong(data-i18n="loading_error.connection_failure") Connection failed.
+  else if status === 401
+    strong(data-i18n="loading_error.unauthorized") You need to be signed in. Do you have cookies disabled?
+  else if status === 403
+    strong(data-i18n="loading_error.forbidden") You do not have the permissions.
+  else if status === 404
+    strong(data-i18n="loading_error.not_found") Not found.
+  else if status === 405
+    strong(data-i18n="loading_error.not_allowed") Method not allowed.
+  else if status === 408
+    strong(data-i18n="loading_error.timeout") Server timeout.
+  else if status === 409
+    strong(data-i18n="loading_error.conflict") Resource conflict.
+  else if status === 422
+    strong(data-i18n="loading_error.bad_input") Bad input.
+  else if status >= 500
+    strong(data-i18n="loading_error.server_error") Server error.
+  else
+    strong(data-i18n="loading_error.unknown") Unknown error.
+
+  if resourceIndex !== undefined
+    button.btn.btn-sm.retry-loading-resource(data-i18n="common.retry", data-resource-index=resourceIndex) Retry
+  if requestIndex !== undefined
+    button.btn.btn-sm.retry-loading-request(data-i18n="common.retry", data-request-index=requestIndex) Retry
+    
\ No newline at end of file
diff --git a/app/views/kinds/CocoView.coffee b/app/views/kinds/CocoView.coffee
index ed81e4a20..768072a26 100644
--- a/app/views/kinds/CocoView.coffee
+++ b/app/views/kinds/CocoView.coffee
@@ -2,6 +2,7 @@ SuperModel = require 'models/SuperModel'
 utils = require 'lib/utils'
 CocoClass = require 'lib/CocoClass'
 loadingScreenTemplate = require 'templates/loading'
+loadingErrorTemplate = require 'templates/loading_error'
 
 visibleModal = null
 waitingModal = null
@@ -18,13 +19,26 @@ module.exports = class CocoView extends Backbone.View
     'click a': 'toggleModal'
     'click button': 'toggleModal'
     'click li': 'toggleModal'
+    'click .retry-loading-resource': 'onRetryResource'
+    'click .retry-loading-request': 'onRetryRequest'
 
   subscriptions: {}
   shortcuts: {}
 
+  # load progress properties
+  loadProgress:
+    num: 0
+    denom: 0
+    showing: false
+    resources: [] # models and collections
+    requests: [] # jqxhr's
+    somethings: [] # everything else
+    progress: 0
+
   # Setup, Teardown
 
   constructor: (options) ->
+    @loadProgress = _.cloneDeep @loadProgress
     @supermodel ?= options?.supermodel or new SuperModel()
     @options = options
     @subscriptions = utils.combineAncestralObject(@, 'subscriptions')
@@ -33,6 +47,7 @@ module.exports = class CocoView extends Backbone.View
     @shortcuts = utils.combineAncestralObject(@, 'shortcuts')
     @subviews = {}
     @listenToShortcuts()
+    @updateProgressBar = _.debounce @updateProgressBar, 100
     # Backbone.Mediator handles subscription setup/teardown automatically
     super options
 
@@ -74,7 +89,7 @@ module.exports = class CocoView extends Backbone.View
     return @template if _.isString(@template)
     @$el.html @template(@getRenderData())
     @afterRender()
-    @showLoading() if @startsLoading
+    @showLoading() if @startsLoading or @loading() # TODO: Remove startsLoading entirely
     @$el.i18n()
     @
 
@@ -89,6 +104,101 @@ module.exports = class CocoView extends Backbone.View
     context
 
   afterRender: ->
+    
+  # Resource and request loading management for any given view
+    
+  addResourceToLoad: (modelOrCollection, name, value=1) ->
+    @loadProgress.resources.push {resource:modelOrCollection, value:value, name:name}
+    @listenToOnce modelOrCollection, 'sync', @updateProgress
+    @listenTo modelOrCollection, 'error', @onResourceLoadFailed
+    @updateProgress()
+    
+  addRequestToLoad: (jqxhr, name, retryFunc, value=1) ->
+    @loadProgress.requests.push {request:jqxhr, value:value, name: name, retryFunc: retryFunc}
+    jqxhr.done @updateProgress
+    jqxhr.fail @onRequestLoadFailed
+
+  addSomethingToLoad: (name, value=1) ->
+    @loadProgress.somethings.push {loaded: false, name: name, value: value}
+    @updateProgress()
+
+  somethingLoaded: (name) ->
+    r = _.find @loadProgress.somethings, {name: name}
+    return console.error 'Could not find something called', name if not r
+    r.loaded = true
+    @updateProgress(name)
+
+  loading: ->
+    return false if @loaded
+    for r in @loadProgress.resources
+      return true if not r.resource.loaded
+    for r in @loadProgress.requests
+      return true if not r.request.status
+    for r in @loadProgress.somethings
+      return true if not r.loaded
+    return false
+
+  updateProgress: =>
+    console.debug 'Loaded', r.name if arguments[0] and r = _.find @loadProgress.resources, {resource:arguments[0]}
+    console.debug 'Loaded', r.name if arguments[2] and r = _.find @loadProgress.requests, {request:arguments[2]}
+    console.debug 'Loaded', r.name if arguments[0] and r = _.find @loadProgress.somethings, {name:arguments[0]}
+
+    denom = 0
+    denom += r.value for r in @loadProgress.resources
+    denom += r.value for r in @loadProgress.requests
+    denom += r.value for r in @loadProgress.somethings
+    num = @loadProgress.num
+    num += r.value for r in @loadProgress.resources when r.resource.loaded
+    num += r.value for r in @loadProgress.requests when r.request.status
+    num += r.value for r in @loadProgress.somethings when r.loaded
+    #console.log 'update progress', @, num, denom, arguments
+    
+    progress = if denom then num / denom else 0
+    # sometimes the denominator isn't known from the outset, so make sure the overall progress only goes up
+    @loadProgress.progress = progress if progress > @loadProgress.progress
+    @updateProgressBar()
+    if num is denom and not @loaded
+      @loaded = true
+      @onLoaded()
+      
+  updateProgressBar: =>
+    prog = "#{parseInt(@loadProgress.progress*100)}%"
+    @$el.find('.loading-screen .progress-bar').css('width', prog)
+
+  onLoaded: ->
+    @render()
+
+  # Error handling for loading
+  
+  onResourceLoadFailed: (resource, jqxhr) ->
+    for r, index in @loadProgress.resources
+      break if r.resource is resource
+    @$el.find('.loading-screen .errors').append(loadingErrorTemplate({
+      status:jqxhr.status,
+      name: r.name
+      resourceIndex: index,
+      responseText: jqxhr.responseText
+    })).i18n()
+  
+  onRetryResource: (e) ->
+    r = @loadProgress.resources[$(e.target).data('resource-index')]
+    r.resource.fetch()
+    $(e.target).closest('.loading-error-alert').remove()
+    
+  onRequestLoadFailed: (jqxhr) =>
+    for r, index in @loadProgress.requests
+      break if r.request is jqxhr
+    @$el.find('.loading-screen .errors').append(loadingErrorTemplate({
+      status:jqxhr.status,
+      name: r.name
+      requestIndex: index,
+      responseText: jqxhr.responseText
+    }))
+    
+  onRetryRequest: (e) ->
+    r = @loadProgress.requests[$(e.target).data('request-index')]
+    @[r.retryFunc]?()
+    $(e.target).closest('.loading-error-alert').remove()
 
   # Modals
 
diff --git a/app/views/play/ladder/ladder_tab.coffee b/app/views/play/ladder/ladder_tab.coffee
index db4eff8ad..ae8c92259 100644
--- a/app/views/play/ladder/ladder_tab.coffee
+++ b/app/views/play/ladder/ladder_tab.coffee
@@ -1,4 +1,5 @@
 CocoView = require 'views/kinds/CocoView'
+CocoClass = require 'lib/CocoClass'
 Level = require 'models/Level'
 LevelSession = require 'models/LevelSession'
 CocoCollection = require 'models/CocoCollection'
@@ -18,7 +19,6 @@ class LevelSessionsCollection extends CocoCollection
 module.exports = class LadderTabView extends CocoView
   id: 'ladder-tab-view'
   template: require 'templates/play/ladder/ladder_tab'
-  startsLoading: true
 
   events:
     'click .connect-facebook': 'onConnectFacebook'
@@ -32,6 +32,7 @@ module.exports = class LadderTabView extends CocoView
 
   constructor: (options, @level, @sessions) ->
     super(options)
+    @addSomethingToLoad("social_network_apis")
     @teams = teamDataFromLevel @level
     @leaderboards = {}
     @refreshLadder()
@@ -39,15 +40,16 @@ module.exports = class LadderTabView extends CocoView
 
   checkFriends: ->
     return if @checked or (not window.FB) or (not window.gapi)
+    @somethingLoaded("social_network_apis")
     @checked = true
-
-    @loadingFacebookFriends = true
+    
+    @addSomethingToLoad("facebook_status")
     FB.getLoginStatus (response) =>
       @facebookStatus = response.status
-      if @facebookStatus is 'connected' then @loadFacebookFriendSessions() else @loadingFacebookFriends = false
+      @somethingLoaded("facebook_status")
+      @loadFacebookFriends() if @facebookStatus is 'connected'
 
     if application.gplusHandler.loggedIn is undefined
-      @loadingGPlusFriends = true
       @listenToOnce(application.gplusHandler, 'checked-state', @gplusSessionStateLoaded)
     else
       @gplusSessionStateLoaded()
@@ -60,16 +62,24 @@ module.exports = class LadderTabView extends CocoView
 
   onConnectedWithFacebook: -> location.reload() if @connecting
 
+  loadFacebookFriends: ->
+    @addSomethingToLoad("facebook_friends")
+    FB.api '/me/friends', @onFacebookFriendsLoaded
+    
+  onFacebookFriendsLoaded: (response) =>
+    @somethingLoaded("facebook_friends")
+    @facebookData = response.data
+    @loadFacebookFriendSessions()
+    
   loadFacebookFriendSessions: ->
-    FB.api '/me/friends', (response) =>
-      @facebookData = response.data
-      levelFrag = "#{@level.get('original')}.#{@level.get('version').major}"
-      url = "/db/level/#{levelFrag}/leaderboard_facebook_friends"
-      $.ajax url, {
-        data: { friendIDs: (f.id for f in @facebookData) }
-        method: 'POST'
-        success: @onFacebookFriendSessionsLoaded
-      }
+    levelFrag = "#{@level.get('original')}.#{@level.get('version').major}"
+    url = "/db/level/#{levelFrag}/leaderboard_facebook_friends"
+    jqxhr = $.ajax url, {
+      data: { friendIDs: (f.id for f in @facebookData) }
+      method: 'POST'
+      success: @onFacebookFriendSessionsLoaded
+    }
+    @addRequestToLoad(jqxhr, 'facebook_friend_sessions', 'loadFacebookFriendSessions')
 
   onFacebookFriendSessionsLoaded: (result) =>
     friendsMap = {}
@@ -79,9 +89,7 @@ module.exports = class LadderTabView extends CocoView
       friend.otherTeam = if friend.team is 'humans' then 'ogres' else 'humans'
       friend.imageSource = "http://graph.facebook.com/#{friend.facebookID}/picture"
     @facebookFriendSessions = result
-    @loadingFacebookFriends = false
-    @renderMaybe()
-
+    
   # GOOGLE PLUS
 
   onConnectGPlus: ->
@@ -93,21 +101,23 @@ module.exports = class LadderTabView extends CocoView
     
   gplusSessionStateLoaded: ->
     if application.gplusHandler.loggedIn
-      @loadingGPlusFriends = true
+      @addSomethingToLoad("gplus_friends")
       application.gplusHandler.loadFriends @gplusFriendsLoaded
-    else
-      @loadingGPlusFriends = false
-      @renderMaybe()
 
   gplusFriendsLoaded: (friends) =>
+    @somethingLoaded("gplus_friends")
     @gplusData = friends.items
+    @loadGPlusFriendSessions()
+    
+  loadGPlusFriendSessions: ->
     levelFrag = "#{@level.get('original')}.#{@level.get('version').major}"
     url = "/db/level/#{levelFrag}/leaderboard_gplus_friends"
-    $.ajax url, {
+    jqxhr = $.ajax url, {
       data: { friendIDs: (f.id for f in @gplusData) }
       method: 'POST'
       success: @onGPlusFriendSessionsLoaded
     }
+    @addRequestToLoad(jqxhr, 'gplus_friend_sessions', 'loadGPlusFriendSessions')
 
   onGPlusFriendSessionsLoaded: (result) =>
     friendsMap = {}
@@ -117,29 +127,15 @@ module.exports = class LadderTabView extends CocoView
       friend.otherTeam = if friend.team is 'humans' then 'ogres' else 'humans'
       friend.imageSource = friendsMap[friend.gplusID].image.url
     @gplusFriendSessions = result
-    @loadingGPlusFriends = false
-    @renderMaybe()
     
   # LADDER LOADING
 
   refreshLadder: ->
-    promises = []
     for team in @teams
-      @leaderboards[team.id]?.off 'sync'
+      @leaderboards[team.id]?.destroy()
       teamSession = _.find @sessions.models, (session) -> session.get('team') is team.id
       @leaderboards[team.id] = new LeaderboardData(@level, team.id, teamSession)
-      promises.push @leaderboards[team.id].promise
-    @loadingLeaderboards = true
-    $.when(promises...).then(@leaderboardsLoaded)
-
-  leaderboardsLoaded: =>
-    @loadingLeaderboards = false
-    @renderMaybe()
-
-  renderMaybe: ->
-    return if @loadingFacebookFriends or @loadingLeaderboards or @loadingGPlusFriends
-    @startsLoading = false
-    @render()
+      @addResourceToLoad @leaderboards[team.id], 'leaderboard', 3
 
   getRenderData: ->
     ctx = super()
@@ -160,9 +156,16 @@ module.exports = class LadderTabView extends CocoView
     sessions.reverse()
     sessions
 
-class LeaderboardData
+class LeaderboardData extends CocoClass
+  ###
+  Consolidates what you need to load for a leaderboard into a single Backbone Model-like object.
+  ###
+  
   constructor: (@level, @team, @session) ->
-    _.extend @, Backbone.Events
+    super()
+    @fetch()
+    
+  fetch: ->
     @topPlayers = new LeaderboardCollection(@level, {order:-1, scoreOffset: HIGHEST_SCORE, team: @team, limit: 20})
     promises = []
     promises.push @topPlayers.fetch()
@@ -173,18 +176,24 @@ class LeaderboardData
       promises.push @playersAbove.fetch()
       @playersBelow = new LeaderboardCollection(@level, {order:-1, scoreOffset: score, limit: 4, team: @team})
       promises.push @playersBelow.fetch()
-      level = "#{level.get('original')}.#{level.get('version').major}"
+      level = "#{@level.get('original')}.#{@level.get('version').major}"
       success = (@myRank) =>
       promises.push $.ajax "/db/level/#{level}/leaderboard_rank?scoreOffset=#{@session.get('totalScore')}&team=#{@team}", {success}
     @promise = $.when(promises...)
     @promise.then @onLoad
+    @promise.fail @onFail
     @promise
 
   onLoad: =>
+    return if @destroyed
     @loaded = true
-    @trigger 'sync'
+    @trigger 'sync', @
     # TODO: cache user ids -> names mapping, and load them here as needed,
     #   and apply them to sessions. Fetching each and every time is too costly.
+  
+  onFail: (resource, jqxhr) =>
+    return if @destroyed
+    @trigger 'error', @, jqxhr
 
   inTopSessions: ->
     return me.id in (session.attributes.creator for session in @topPlayers.models)
@@ -201,3 +210,7 @@ class LeaderboardData
       startRank = @myRank - 4
       session.rank = startRank + i for session, i in l
     l
+
+  allResources: ->
+    resources = [@topPlayers, @playersAbove, @playersBelow]
+    return (r for r in resources when r)
\ No newline at end of file
diff --git a/app/views/play/ladder_view.coffee b/app/views/play/ladder_view.coffee
index 6d1e90313..58366fa58 100644
--- a/app/views/play/ladder_view.coffee
+++ b/app/views/play/ladder_view.coffee
@@ -24,7 +24,6 @@ class LevelSessionsCollection extends CocoCollection
 module.exports = class LadderView extends RootView
   id: 'ladder-view'
   template: require 'templates/play/ladder'
-  startsLoading: true
 
   subscriptions:
     'application:idle-changed': 'onIdleChanged'
@@ -38,18 +37,18 @@ module.exports = class LadderView extends RootView
   constructor: (options, @levelID) ->
     super(options)
     @level = new Level(_id:@levelID)
-    p1 = @level.fetch()
+    @level.fetch()
     @sessions = new LevelSessionsCollection(levelID)
-    p2 = @sessions.fetch({})
+    @sessions.fetch({})
+    @addResourceToLoad(@sessions, 'your_sessions')
+    @addResourceToLoad(@level, 'level')
     @simulator = new Simulator()
     @listenTo(@simulator, 'statusUpdate', @updateSimulationStatus)
     @teams = []
-    $.when(p1, p2).then @onLoaded
 
-  onLoaded: =>
+  onLoaded: ->
     @teams = teamDataFromLevel @level
-    @startsLoading = false
-    @render()
+    super()
 
   getRenderData: ->
     ctx = super()
@@ -63,7 +62,7 @@ module.exports = class LadderView extends RootView
 
   afterRender: ->
     super()
-    return if @startsLoading
+    return if @loading()
     @insertSubView(@ladderTab = new LadderTabView({}, @level, @sessions))
     @insertSubView(@myMatchesTab = new MyMatchesTabView({}, @level, @sessions))
     @refreshInterval = setInterval(@fetchSessionsAndRefreshViews.bind(@), 10 * 1000)
@@ -72,7 +71,7 @@ module.exports = class LadderView extends RootView
       @showPlayModal(hash) if @sessions.loaded
 
   fetchSessionsAndRefreshViews: ->
-    return if @destroyed or application.userIsIdle or @$el.find('#simulate.active').length or (new Date() - 2000 < @lastRefreshTime) or @startsLoading
+    return if @destroyed or application.userIsIdle or @$el.find('#simulate.active').length or (new Date() - 2000 < @lastRefreshTime) or @loading()
     @sessions.fetch({"success": @refreshViews})
 
   refreshViews: =>
diff --git a/app/views/play/level/tome/spell_view.coffee b/app/views/play/level/tome/spell_view.coffee
index 17a813459..4951b6915 100644
--- a/app/views/play/level/tome/spell_view.coffee
+++ b/app/views/play/level/tome/spell_view.coffee
@@ -63,7 +63,7 @@ module.exports = class SpellView extends View
       @createFirepad()
     else
       # needs to happen after the code generating this view is complete
-      setTimeout @onLoaded, 1
+      setTimeout @onAllLoaded, 1
 
   createACE: ->
     # Test themes and settings here: http://ace.ajax.org/build/kitchen-sink.html
@@ -178,9 +178,9 @@ module.exports = class SpellView extends View
     else
       @ace.setValue @previousSource
       @ace.clearSelection()
-    @onLoaded()
+    @onAllLoaded()
 
-  onLoaded: =>
+  onAllLoaded: =>
     @spell.transpile @spell.source
     @spell.loaded = true
     Backbone.Mediator.publish 'tome:spell-loaded', spell: @spell