NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

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:

ExceptionThrown byWhen
InviteExpiredExceptionAcceptInviteActionThe invite’s expires_at has passed.
InviteExhaustedExceptionAcceptInviteActionuses has reached max_uses — including a single-use invite already consumed.
InviteEmailMismatchExceptionAcceptInviteActionAn email-targeted invite gets a different or missing email (compared case-insensitively).
InviteNotFoundExceptionTeams::invites()->accept()No invite matches the code, the invite was revoked, or its team was deleted.
InviteNotFoundExceptionfor($t)->invites()->resend() / revoke()The invite belongs to another team.
JoinRequestNotFoundExceptionfor($t)->joinRequests()->approve() / deny()The request belongs to another team.
JoinRequestNotPendingExceptionfor($t)->joinRequests()->approve() / deny()The request was already approved, denied or expired — including by a concurrent responder.
MemberNotFoundExceptionfor($t)->members()->changeRole()The model is not a member of the team.
MemberNotFoundExceptionfor($t)->members()->grant($m)->assignRole() …An addition for a model with no membership of the team, or an expired one.
TeamContextExceptionScopedToTeam, UniqueInTeam, switchTo(), setCurrentMany(), teams.contextNo 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.
TeamHierarchyExceptionsaving a team with a parentNesting is off, the parent is the team or one of its descendants, or the move goes deeper than nesting.max_depth.
TeamsExceptionjoinRequests()->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:

KeyEnglish default
teams::errors.already_memberThe given model is already a member of team #:team.
teams::errors.invite_expiredThe invite ":code" has expired.
teams::errors.invite_email_mismatchThis invite is addressed to ":email".
teams::errors.invite_exhaustedThe invite ":code" has reached its usage limit.
teams::errors.invite_not_foundNo invite was found for the code ":code".
teams::errors.invite_team_missingThe team of invite #:id no longer exists.
teams::errors.invite_unavailableInvite #:id is no longer available.
teams::errors.invite_not_in_teamInvite #:id does not belong to this team.
teams::errors.join_request_not_pendingJoin request #:id has already been resolved.
teams::errors.join_request_not_in_teamJoin request #:id does not belong to this team.
teams::errors.member_not_foundThe given model is not a member of team #:team.
teams::errors.join_policy_forbids_requestsThis team is invite-only and does not accept join requests.
teams::errors.invalid_config_valueConfiguration value [:key] must be :expected, [:given] given.
teams::errors.config_expectations.positive_intervala positive interval such as "7 days"
teams::errors.config_expectations.non_negative_intervala zero or positive interval such as "30 days"
teams::errors.max_seats_reachedThis team has reached its seat limit (:count seat).|This team has reached its seat limit (:count seats).
teams::errors.context_missingNo team context is set.
teams::errors.context_ambiguousSeveral teams are in context: name the team explicitly.
teams::errors.context_outsideTeam #:team is outside the current team context.
teams::errors.context_not_accessibleThe user is not an active member of team #:team.
teams::errors.context_no_userSetting several teams needs a user to check memberships against.
teams::errors.context_switch_no_userSwitching teams needs a user to check the membership against.
teams::errors.context_revokedThe member who set the team context may no longer act in team #:team.
teams::errors.context_actor_missingThe member who set the team context no longer exists.
teams::errors.context_unsaved_teamA team must be saved before it can be the current team.
teams::errors.context_route_parameterThe route has no [:parameter] parameter to read the team from.
teams::errors.start_dates_disabledMembership start dates are disabled: turn on teams.members.start_dates.
teams::errors.membership_windowA membership must start before it expires.
teams::errors.nesting_disabledNested teams are disabled.
teams::errors.nesting_cycleTeam #: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_keyThe scoped key ":suffix" may not contain "." or ".." segments.
teams::errors.permissions_bridge_not_installedteams.permissions.bridge is on, but roundly-consulting/permissions-for-laravel ^2.1 is not installed.
teams::errors.permissions_bridge_assignments_offteams.permissions.bridge needs scoped assignments: turn on permissions.assignments.enabled and run the permissions migrations.
teams::errors.permissions_bridge_offMember grants need the permissions bridge: turn on teams.permissions.bridge.
teams::validation.unique_in_teamThe :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 locales

Show 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 crypto

By 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.