laravel/framework · error · TypeError

{self::class}::bind(): Argument #2 ($concrete) must be of ty

Error message

{self::class}::bind(): Argument #2 ($concrete) must be of type Closure|string|null

What it means

Thrown as a TypeError by Container::bind when the $concrete argument is neither a Closure nor a string nor null (e.g. an integer, array, object instance). The bind signature accepts mixed only because the first argument can be a Closure; once it is a string the concrete must be Closure|string|null.

Source

Thrown at src/Illuminate/Container/Container.php:381

                $abstract, $concrete, $shared
            );
        }

        $this->dropStaleInstances($abstract);

        // If no concrete type was given, we will simply set the concrete type to the
        // abstract type. After that, the concrete type to be registered as shared
        // without being forced to state their classes in both of the parameters.
        if (is_null($concrete)) {
            $concrete = $abstract;
        }

        // If the factory is not a Closure, it means it is just a class name which is
        // bound into this container to the abstract type and we will just wrap it
        // up inside its own Closure to give us more convenience when extending.
        if (! $concrete instanceof Closure) {
            if (! is_string($concrete)) {
                throw new TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
            }

            $concrete = $this->getClosure($abstract, $concrete);
        }

        $this->bindings[$abstract] = ['concrete' => $concrete, 'shared' => $shared];

        // If the abstract type was already resolved in this container we'll fire the
        // rebound listener so that any objects which have already gotten resolved
        // can have their copy of the object updated via the listener callbacks.
        if ($this->resolved($abstract)) {
            $this->rebound($abstract);
        }
    }

    /**
     * Get the Closure to be used when building a type.
     *

View on GitHub (pinned to bd6b5437e6)

Solutions

  1. Pass a class name string: $container->bind(PaymentContract::class, StripeGateway::class).
  2. Pass a Closure: $container->bind('key', fn ($app) => new MyService($app['config'])).
  3. To register a pre-built object use instance(): $container->instance('key', $object).
  4. Verify the variable you are passing is a string class name or Closure before calling bind.

Example fix

// before
$container->bind('cache.store', new FileStore(...));

// after
$container->instance('cache.store', new FileStore(...));
// or
$container->bind('cache.store', fn () => new FileStore(...));
Defensive patterns

Strategy: type-guard

Validate before calling

if (! ($concrete instanceof \Closure || is_string($concrete) || is_null($concrete))) {
    throw new \TypeError('concrete must be Closure|string|null');
}
$container->bind($abstract, $concrete);

Type guard

function isBindConcrete(mixed $c): bool
{
    return $c instanceof \Closure || is_string($c) || $c === null;
}

Try / catch

try {
    $container->bind($key, $concrete);
} catch (\TypeError $e) {
    if (str_contains($e->getMessage(), 'must be of type Closure|string|null')) {
        $container->instance($key, $concrete); // fallback for objects
    } else { throw $e; }
}

Prevention

When it happens

Trigger: Calling $container->bind('key', 42), $container->bind('key', ['a','b']), or $container->bind('key', $someObject) (object instance rather than class name string).

Common situations: Confusing bind() with instance() (use instance() to register an already-built object); passing a config array as concrete; dynamic value resolved to a non-string; copy-paste mistake between singleton($abstract, $concreteClass) and a real instance.

Related errors


AI-assisted analysis of laravel/framework@bd6b5437e6 (2026-08-06). Data as JSON: /data/errors/707a75d527913c2b.json. Report an issue: GitHub.