Exceptions
Every exception lives in RoundlyConsulting\Teams\Exceptions and extends TeamsException, which extends RuntimeException. Catch the base type for any package error, or a specific one for targeted handling:
| Exception | Thrown by | When |
|---|---|---|
InviteExpiredException | AcceptInviteAction | The invite’s expires_at has passed. |
InviteExhaustedException | AcceptInviteAction | uses has reached max_uses — including a single-use invite already consumed. |
InviteEmailMismatchException | AcceptInviteAction | An email-targeted invite gets a different or missing email (compared case-insensitively). |
InviteNotFoundException | Teams::invites()->accept() | No invite matches the code, the invite was revoked, or its team was deleted. |
InviteNotFoundException | for($t)->invites()->resend() / revoke() | The invite belongs to another team. |
JoinRequestNotFoundException | for($t)->joinRequests()->approve() / deny() | The request belongs to another team. |
JoinRequestNotPendingException | for($t)->joinRequests()->approve() / deny() | The request was already approved, denied or expired — including by a concurrent responder. |
MemberNotFoundException | for($t)->members()->changeRole() | The model is not a member of the team. |
MemberNotFoundException | for($t)->members()->grant($m)->assignRole() … | An addition for a model with no membership of the team, or an expired one. |
TeamContextException | ScopedToTeam, UniqueInTeam, switchTo(), setCurrentMany(), teams.context | No context, several teams and none named, a row outside the context, a user who is not an active member, no user to check, an unsaved team, a route without the team parameter — or, at a queued job’s start, a member who lost access after dispatch. |
TeamHierarchyException | saving a team with a parent | Nesting is off, the parent is the team or one of its descendants, or the move goes deeper than nesting.max_depth. |
TeamsException | joinRequests()->open(), members()->add(), transferOwnershipTo(), scopedKey(), members()->grant() | The base class. Thrown directly for an invite-only team’s join request, a join request from an active member, a reached seat limit, a startsAt while start dates are off, a start not before the expiry, an unsafe scopedKey() suffix and grant() while the permissions bridge is off. |
use RoundlyConsulting\Teams\Exceptions\InviteEmailMismatchException;
use RoundlyConsulting\Teams\Exceptions\InviteExhaustedException;
use RoundlyConsulting\Teams\Exceptions\InviteExpiredException;
use RoundlyConsulting\Teams\Exceptions\InviteNotFoundException;
use RoundlyConsulting\Teams\Exceptions\TeamsException;
try {
Teams::invites()->accept($code, $user, email: $user->email);
} catch (InviteExpiredException) {
// the link has expired
} catch (InviteExhaustedException) {
// no seats left, or a single-use link already consumed
} catch (InviteEmailMismatchException) {
// the wrong recipient
} catch (InviteNotFoundException) {
// an unknown code, a revoked invite or a deleted team
} catch (TeamsException $e) {
// any other teams error — e.g. a seat limit
}The invite checks run expired → exhausted → revoked → email mismatch → deleted team, so the first failing condition is the one you catch.
Translated messages
The invite, join-request, membership, seat-limit, team-context, nesting, permissions-bridge and interval-config messages — and the UniqueInTeam validation message — come from the teams:: translation namespace and follow the app locale; English and Slovak ship:
| Key | English default |
|---|---|
teams::errors.already_member | The given model is already a member of team #:team. |
teams::errors.invite_expired | The invite ":code" has expired. |
teams::errors.invite_email_mismatch | This invite is addressed to ":email". |
teams::errors.invite_exhausted | The invite ":code" has reached its usage limit. |
teams::errors.invite_not_found | No invite was found for the code ":code". |
teams::errors.invite_team_missing | The team of invite #:id no longer exists. |
teams::errors.invite_unavailable | Invite #:id is no longer available. |
teams::errors.invite_not_in_team | Invite #:id does not belong to this team. |
teams::errors.join_request_not_pending | Join request #:id has already been resolved. |
teams::errors.join_request_not_in_team | Join request #:id does not belong to this team. |
teams::errors.member_not_found | The given model is not a member of team #:team. |
teams::errors.join_policy_forbids_requests | This team is invite-only and does not accept join requests. |
teams::errors.invalid_config_value | Configuration value [:key] must be :expected, [:given] given. |
teams::errors.config_expectations.positive_interval | a positive interval such as "7 days" |
teams::errors.config_expectations.non_negative_interval | a zero or positive interval such as "30 days" |
teams::errors.max_seats_reached | This team has reached its seat limit (:count seat).|This team has reached its seat limit (:count seats). |
teams::errors.context_missing | No team context is set. |
teams::errors.context_ambiguous | Several teams are in context: name the team explicitly. |
teams::errors.context_outside | Team #:team is outside the current team context. |
teams::errors.context_not_accessible | The user is not an active member of team #:team. |
teams::errors.context_no_user | Setting several teams needs a user to check memberships against. |
teams::errors.context_switch_no_user | Switching teams needs a user to check the membership against. |
teams::errors.context_revoked | The member who set the team context may no longer act in team #:team. |
teams::errors.context_actor_missing | The member who set the team context no longer exists. |
teams::errors.context_unsaved_team | A team must be saved before it can be the current team. |
teams::errors.context_route_parameter | The route has no [:parameter] parameter to read the team from. |
teams::errors.start_dates_disabled | Membership start dates are disabled: turn on teams.members.start_dates. |
teams::errors.membership_window | A membership must start before it expires. |
teams::errors.nesting_disabled | Nested teams are disabled. |
teams::errors.nesting_cycle | Team #:team cannot be placed under itself or one of its descendants. |
teams::errors.nesting_too_deep | {1} A team may sit at most :count level below its root team.|[0,*] A team may sit at most :count levels below its root team. |
teams::errors.unsafe_scoped_key | The scoped key ":suffix" may not contain "." or ".." segments. |
teams::errors.permissions_bridge_not_installed | teams.permissions.bridge is on, but roundly-consulting/permissions-for-laravel ^2.1 is not installed. |
teams::errors.permissions_bridge_assignments_off | teams.permissions.bridge needs scoped assignments: turn on permissions.assignments.enabled and run the permissions migrations. |
teams::errors.permissions_bridge_off | Member grants need the permissions bridge: turn on teams.permissions.bridge. |
teams::validation.unique_in_team | The :attribute has already been taken in this team. |
max_seats_reached is rendered with trans_choice(): English uses singular|plural, Slovak {0} …|{1} …|[2,4] …|[5,*] …. An override may keep a single line without | — it is rendered as is.
php artisan vendor:publish --tag="teams-translations"
# edit lang/vendor/teams/en/errors.php or lang/vendor/teams/sk/errors.php, or add other localesShow your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.