Doing a join doesn't return the query interface, but rather the
alias that was assigned to the joined table. Since variable
assignment is processed left to right once it processes the ::join
method your $users variable now contains the table alias.
What you want to do is stop your chaining before any methods that
don't return the SelectQuery object you are building:
$query = db_select('user_badges_user', 'ubu')
->fields('u', array('uid', 'name'))
->condition('ubu.bid', $badge->bid); // Up to here you
get a SelectQueryInterface returned and assigned to query
$query->join('users', 'u', 'u.uid = ubu.uid'); // Calls the join
method on $query, but doesn't assign the return;
$users = $query->execute(); // Returns the
DatabaseStatementInterface that you pull your results from
Generally I try to add joins at the beginning of the construction.
It seems to make it easier to read (it seems most of Drupal's core
code does the same), so I would write this query like this:
$query = db_select('user_badges_user', 'ubu');
$query->join('users', 'u', 'u.uid = ubu.uid');
$query->fields('u', array('uid', 'name'))
->condition('ubu.bid', $badge->bid);
$users = $query->execute();
You could assign $users when you add the fields and conditions, but
this does make it easier if you want to add anything else to the
query down the road.
Easiest thing to remember is that if you get a non-member/non-object
error, then the item right above that doesn't return the proper
class and you should stop variable assignment there and instead just
call the method without assignment (ie: $query->join('users',
'u', 'u.uid = ubu.uid')).
Chaining might look a little better and save a few keystrokes, but
it really doesn't bring any performance enhancements and can keep
headaches to a minimum, especially when working on classes you
aren't that familiar with.